First public release
This commit is contained in:
		
							parent
							
								
									963cae3756
								
							
						
					
					
						commit
						64333fdb4a
					
				
					 14 changed files with 1580 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | .vscode/ | ||||||
|  | __pycache__/ | ||||||
|  | config.cfg | ||||||
							
								
								
									
										306
									
								
								dance.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								dance.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,306 @@ | ||||||
|  | import asyncio | ||||||
|  | import json | ||||||
|  | import logging | ||||||
|  | import re | ||||||
|  | import socket | ||||||
|  | import time | ||||||
|  | from configparser import ConfigParser | ||||||
|  | from enum import Enum | ||||||
|  | 
 | ||||||
|  | import hid | ||||||
|  | from aiohttp import WSMsgType, web | ||||||
|  | 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 | ||||||
|  | 
 | ||||||
|  | logging.getLogger('asyncio').setLevel(logging.WARNING) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WsCommand(Enum): | ||||||
|  |     GET_JOYCON_LIST = 'get_joycon_list' | ||||||
|  |     CONNECT_JOYCON = 'connect_joycon' | ||||||
|  |     DISCONNECT_JOYCON = 'disconnect_joycon' | ||||||
|  |     UPDATE_JOYCON_STATE = 'update_joycon_state' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PairingMethod(Enum): | ||||||
|  |     DEFAULT = 'default' | ||||||
|  |     FAST = 'fast' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | REGEX_PAIRING_CODE = re.compile(r'^\d{6}$') | ||||||
|  | REGEX_LOCAL_IP_ADDRESS = re.compile(r'^192\.168\.((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.)(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def get_device_ids(): | ||||||
|  |     devices = hid.enumerate(JOYCON_VENDOR_ID, 0) | ||||||
|  | 
 | ||||||
|  |     out = [] | ||||||
|  |     for device in devices: | ||||||
|  |         vendor_id = device['vendor_id'] | ||||||
|  |         product_id = device['product_id'] | ||||||
|  |         product_string = device['product_string'] | ||||||
|  |         serial = device.get('serial') or device.get('serial_number') | ||||||
|  | 
 | ||||||
|  |         if product_id not in JOYCON_PRODUCT_IDS: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         if not product_string: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         out.append({ | ||||||
|  |             'vendor_id': vendor_id, | ||||||
|  |             'product_id': product_id, | ||||||
|  |             'serial': serial, | ||||||
|  |             'product_string': product_string, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     return out | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def get_joycon_list(app): | ||||||
|  |     joycons = [] | ||||||
|  |     devices = await get_device_ids() | ||||||
|  | 
 | ||||||
|  |     for dev in devices: | ||||||
|  |         if dev['serial'] in app['joycons_info']: | ||||||
|  |             info = app['joycons_info'][dev['serial']] | ||||||
|  |         else: | ||||||
|  |             joycon = JoyCon(dev['vendor_id'], dev['product_id'], dev['serial']) | ||||||
|  |             # Wait for initial data | ||||||
|  |             for _ in range(3): | ||||||
|  |                 time.sleep(0.05) | ||||||
|  |                 battery_level = joycon.get_battery_level() | ||||||
|  |                 if battery_level > 0: | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |             color = '#%02x%02x%02x' % joycon.color_body | ||||||
|  |             joycon.__del__() | ||||||
|  | 
 | ||||||
|  |             info = { | ||||||
|  |                 'vendor_id': dev['vendor_id'], | ||||||
|  |                 'product_id': dev['product_id'], | ||||||
|  |                 'serial': dev['serial'], | ||||||
|  |                 'name': dev['product_string'], | ||||||
|  |                 'color': color, | ||||||
|  |                 'battery_level': battery_level, | ||||||
|  |                 'is_left': joycon.is_left(), | ||||||
|  |                 'state': PairingState.IDLE.value, | ||||||
|  |                 'pairing_code': '', | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             app['joycons_info'][dev['serial']] = info | ||||||
|  | 
 | ||||||
|  |         joycons.append(info) | ||||||
|  | 
 | ||||||
|  |     return sorted(joycons, key=lambda x: (x['name'], x['color'], x['serial'])) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def connect_joycon(app, ws, data): | ||||||
|  |     async def on_joydance_state_changed(serial, state): | ||||||
|  |         print(serial, state) | ||||||
|  |         app['joycons_info'][serial]['state'] = state.value | ||||||
|  |         try: | ||||||
|  |             await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, app['joycons_info'][serial]) | ||||||
|  |         except Exception as e: | ||||||
|  |             print(e) | ||||||
|  | 
 | ||||||
|  |     print(data) | ||||||
|  | 
 | ||||||
|  |     serial = data['joycon_serial'] | ||||||
|  |     product_id = app['joycons_info'][serial]['product_id'] | ||||||
|  |     vendor_id = app['joycons_info'][serial]['vendor_id'] | ||||||
|  | 
 | ||||||
|  |     pairing_method = data['pairing_method'] | ||||||
|  |     host_ip_addr = data['host_ip_addr'] | ||||||
|  |     console_ip_addr = data['console_ip_addr'] | ||||||
|  |     pairing_code = data['pairing_code'] | ||||||
|  | 
 | ||||||
|  |     if not is_valid_pairing_method(pairing_method): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     if pairing_method == PairingMethod.DEFAULT.value: | ||||||
|  |         if not is_valid_ip_address(host_ip_addr) or not is_valid_pairing_code(pairing_code): | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |     if pairing_method == PairingMethod.FAST.value and not is_valid_ip_address(console_ip_addr): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     config_parser = parse_config() | ||||||
|  |     config = dict(config_parser.items('joydance')) | ||||||
|  |     config['pairing_code'] = pairing_code | ||||||
|  |     config['pairing_method'] = pairing_method | ||||||
|  |     config['host_ip_addr'] = host_ip_addr | ||||||
|  |     config['console_ip_addr'] = console_ip_addr | ||||||
|  |     config_parser['joydance'] = config | ||||||
|  |     save_config(config_parser) | ||||||
|  | 
 | ||||||
|  |     app['joycons_info'][serial]['pairing_code'] = pairing_code | ||||||
|  |     joycon = ButtonEventJoyCon(vendor_id, product_id, serial) | ||||||
|  | 
 | ||||||
|  |     if pairing_method == PairingMethod.DEFAULT.value: | ||||||
|  |         console_ip_addr = None | ||||||
|  | 
 | ||||||
|  |     joydance = JoyDance( | ||||||
|  |             joycon, | ||||||
|  |             pairing_code=pairing_code, | ||||||
|  |             host_ip_addr=host_ip_addr, | ||||||
|  |             console_ip_addr=console_ip_addr, | ||||||
|  |             on_state_changed=on_joydance_state_changed, | ||||||
|  |             accel_acquisition_freq_hz=config['accel_acquisition_freq_hz'], | ||||||
|  |             accel_acquisition_latency=config['accel_acquisition_latency'], | ||||||
|  |             accel_max_range=config['accel_max_range'], | ||||||
|  |     ) | ||||||
|  |     app['joydance_connections'][serial] = joydance | ||||||
|  | 
 | ||||||
|  |     asyncio.create_task(joydance.pair()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | 
 | ||||||
|  |     await joydance.disconnect() | ||||||
|  |     try: | ||||||
|  |         await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, { | ||||||
|  |             'joycon_serial': serial, | ||||||
|  |             'state': PairingState.IDLE.value, | ||||||
|  |         }) | ||||||
|  |     except Exception: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def parse_config(): | ||||||
|  |     parser = ConfigParser() | ||||||
|  |     parser.read('config.cfg') | ||||||
|  | 
 | ||||||
|  |     if 'joydance' not in parser: | ||||||
|  |         parser['joydance'] = DEFAULT_CONFIG | ||||||
|  |     else: | ||||||
|  |         tmp_config = DEFAULT_CONFIG.copy() | ||||||
|  |         for key in tmp_config: | ||||||
|  |             if key in parser['joydance']: | ||||||
|  |                 val = parser['joydance'][key] | ||||||
|  |                 if key == 'pairing_method': | ||||||
|  |                     if not is_valid_pairing_method(val): | ||||||
|  |                         val = PairingMethod.DEFAULT.value | ||||||
|  |                 elif key == 'host_ip_addr' or key == 'console_ip_addr': | ||||||
|  |                     if not(is_valid_ip_address(val)): | ||||||
|  |                         val = '' | ||||||
|  |                 elif key == 'pairing_code': | ||||||
|  |                     if not is_valid_pairing_code(val): | ||||||
|  |                         val = '' | ||||||
|  |                 elif key.startswith('accel_'): | ||||||
|  |                     try: | ||||||
|  |                         val = int(val) | ||||||
|  |                     except Exception: | ||||||
|  |                         val = DEFAULT_CONFIG[key] | ||||||
|  | 
 | ||||||
|  |                 tmp_config[key] = val | ||||||
|  | 
 | ||||||
|  |         parser['joydance'] = tmp_config | ||||||
|  | 
 | ||||||
|  |     if not parser['joydance']['host_ip_addr']: | ||||||
|  |         host_ip_addr = get_host_ip() | ||||||
|  |         if host_ip_addr: | ||||||
|  |             parser['joydance']['host_ip_addr'] = host_ip_addr | ||||||
|  | 
 | ||||||
|  |     save_config(parser) | ||||||
|  |     return parser | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_valid_pairing_code(val): | ||||||
|  |     return re.match(REGEX_PAIRING_CODE, val) is not None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_valid_ip_address(val): | ||||||
|  |     return re.match(REGEX_LOCAL_IP_ADDRESS, val) is not None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_valid_pairing_method(val): | ||||||
|  |     return val in [PairingMethod.DEFAULT.value, PairingMethod.FAST.value] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_host_ip(): | ||||||
|  |     try: | ||||||
|  |         for ip in socket.gethostbyname_ex(socket.gethostname())[2]: | ||||||
|  |             if ip.startswith('192.168'): | ||||||
|  |                 return ip | ||||||
|  |     except Exception: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def save_config(parser): | ||||||
|  |     with open('config.cfg', 'w') as fp: | ||||||
|  |         parser.write(fp) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def html_handler(request): | ||||||
|  |     config = dict((parse_config()).items('joydance')) | ||||||
|  |     with open('static/index.html', 'r') as f: | ||||||
|  |         html = f.read() | ||||||
|  |         html = html.replace('[[CONFIG]]', json.dumps(config)) | ||||||
|  |         html = html.replace('[[VERSION]]', JOYDANCE_VERSION) | ||||||
|  |         return web.Response(text=html, content_type='text/html') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def ws_send_response(ws, cmd, data): | ||||||
|  |     resp = { | ||||||
|  |         'cmd': 'resp_' + cmd.value, | ||||||
|  |         'data': data, | ||||||
|  |     } | ||||||
|  |     await ws.send_json(resp) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def websocket_handler(request): | ||||||
|  |     ws = web.WebSocketResponse() | ||||||
|  |     await ws.prepare(request) | ||||||
|  | 
 | ||||||
|  |     async for msg in ws: | ||||||
|  |         if msg.type == WSMsgType.TEXT: | ||||||
|  |             msg = msg.json() | ||||||
|  |             try: | ||||||
|  |                 cmd = WsCommand(msg['cmd']) | ||||||
|  |             except ValueError: | ||||||
|  |                 print('Invalid cmd:', msg['cmd']) | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             if cmd == WsCommand.GET_JOYCON_LIST: | ||||||
|  |                 joycon_list = await get_joycon_list(request.app) | ||||||
|  |                 await ws_send_response(ws, cmd, joycon_list) | ||||||
|  |             elif cmd == WsCommand.CONNECT_JOYCON: | ||||||
|  |                 await connect_joycon(request.app, ws, msg['data']) | ||||||
|  |                 await ws_send_response(ws, cmd, {}) | ||||||
|  |             elif cmd == WsCommand.DISCONNECT_JOYCON: | ||||||
|  |                 await disconnect_joycon(request.app, ws, msg['data']) | ||||||
|  |                 await ws_send_response(ws, cmd, {}) | ||||||
|  |         elif msg.type == WSMsgType.ERROR: | ||||||
|  |             print('ws connection closed with exception %s' % | ||||||
|  |                   ws.exception()) | ||||||
|  | 
 | ||||||
|  |     return ws | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def favicon_handler(request): | ||||||
|  |     return web.FileResponse('static/favicon.png') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app = web.Application() | ||||||
|  | app['joydance_connections'] = {} | ||||||
|  | app['joycons_info'] = {} | ||||||
|  | 
 | ||||||
|  | app.add_routes([ | ||||||
|  |     web.get('/', html_handler), | ||||||
|  |     web.get('/favicon.png', favicon_handler), | ||||||
|  |     web.get('/ws', websocket_handler), | ||||||
|  |     web.static('/css', 'static/css'), | ||||||
|  |     web.static('/js', 'static/js'), | ||||||
|  | ]) | ||||||
|  | 
 | ||||||
|  | web.run_app(app, port=32623) | ||||||
							
								
								
									
										413
									
								
								joydance/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								joydance/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,413 @@ | ||||||
|  | import asyncio | ||||||
|  | import json | ||||||
|  | import random | ||||||
|  | import socket | ||||||
|  | import ssl | ||||||
|  | import time | ||||||
|  | from enum import Enum | ||||||
|  | 
 | ||||||
|  | import aiohttp | ||||||
|  | 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) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PairingState(Enum): | ||||||
|  |     IDLE = 0 | ||||||
|  |     GETTING_TOKEN = 1 | ||||||
|  |     PAIRING = 2 | ||||||
|  |     CONNECTING = 3 | ||||||
|  |     CONNECTED = 4 | ||||||
|  |     DISCONNECTING = 5 | ||||||
|  |     DISCONNECTED = 10 | ||||||
|  | 
 | ||||||
|  |     ERROR_JOYCON = 101 | ||||||
|  |     ERROR_CONNECTION = 102 | ||||||
|  |     ERROR_INVALID_PAIRING_CODE = 103 | ||||||
|  |     ERROR_PUNCH_PAIRING = 104 | ||||||
|  |     ERROR_HOLE_PUNCHING = 105 | ||||||
|  |     ERROR_CONSOLE_CONNECTION = 106 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class JoyDance: | ||||||
|  |     def __init__( | ||||||
|  |             self, | ||||||
|  |             joycon, | ||||||
|  |             pairing_code=None, | ||||||
|  |             host_ip_addr=None, | ||||||
|  |             console_ip_addr=None, | ||||||
|  |             accel_acquisition_freq_hz=ACCEL_ACQUISITION_FREQ_HZ, | ||||||
|  |             accel_acquisition_latency=ACCEL_ACQUISITION_LATENCY, | ||||||
|  |             accel_max_range=ACCEL_MAX_RANGE, | ||||||
|  |             on_state_changed=None): | ||||||
|  |         self.joycon = joycon | ||||||
|  |         self.joycon_is_left = joycon.is_left() | ||||||
|  | 
 | ||||||
|  |         if on_state_changed: | ||||||
|  |             self.on_state_changed = on_state_changed | ||||||
|  | 
 | ||||||
|  |         self.pairing_code = pairing_code | ||||||
|  |         self.host_ip_addr = host_ip_addr | ||||||
|  |         self.console_ip_addr = console_ip_addr | ||||||
|  |         self.host_port = self.get_random_port() | ||||||
|  | 
 | ||||||
|  |         self.accel_acquisition_freq_hz = accel_acquisition_freq_hz | ||||||
|  |         self.accel_acquisition_latency = accel_acquisition_latency | ||||||
|  |         self.accel_max_range = accel_max_range | ||||||
|  | 
 | ||||||
|  |         self.number_of_accels_sent = 0 | ||||||
|  |         self.should_start_accelerometer = False | ||||||
|  |         self.is_input_allowed = False | ||||||
|  |         self.available_shortcuts = set() | ||||||
|  | 
 | ||||||
|  |         self.ws = None | ||||||
|  |         self.disconnected = False | ||||||
|  | 
 | ||||||
|  |         self.headers = { | ||||||
|  |             'Ubi-AppId': UBI_APP_ID, | ||||||
|  |             'X-SkuId': UBI_SKU_ID, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.console_conn = None | ||||||
|  | 
 | ||||||
|  |     def get_random_port(self): | ||||||
|  |         ''' Randomize a port number, to be used in hole_punching() later ''' | ||||||
|  |         return random.randrange(39000, 39999) | ||||||
|  | 
 | ||||||
|  |     async def on_state_changed(state): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     async def get_access_token(self): | ||||||
|  |         ''' Log in using a guest account, pre-defined by Ubisoft ''' | ||||||
|  |         headers = { | ||||||
|  |             'Authorization': 'UbiMobile_v1 t=NTNjNWRjZGMtZjA2Yy00MTdmLWJkMjctOTNhZTcxNzU1OTkyOlcwM0N5eGZldlBTeFByK3hSa2hhQ05SMXZtdz06UjNWbGMzUmZaVzB3TjJOYTpNakF5TVMweE1DMHlOMVF3TVRvME5sbz0=', | ||||||
|  |             'Ubi-AppId': UBI_APP_ID, | ||||||
|  |             'User-Agent': 'UbiServices_SDK_Unity_Light_Mobile_2018.Release.16_ANDROID64_dynamic', | ||||||
|  |             'Ubi-RequestedPlatformType': 'ubimobile', | ||||||
|  |             'Content-Type': 'application/json', | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         async with aiohttp.ClientSession(headers=headers) as session: | ||||||
|  |             async with session.post('https://public-ubiservices.ubi.com/v1/profiles/sessions', json={}, ssl=False) as resp: | ||||||
|  |                 if resp.status != 200: | ||||||
|  |                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONNECTION) | ||||||
|  |                     raise Exception('ERROR: Couldn\'t get access token!') | ||||||
|  | 
 | ||||||
|  |                 # Add ticket to headers | ||||||
|  |                 json_body = await resp.json() | ||||||
|  |                 self.headers['Authorization'] = 'Ubi_v1 ' + json_body['ticket'] | ||||||
|  | 
 | ||||||
|  |     async def send_pairing_code(self): | ||||||
|  |         ''' Send pairing code to JD server ''' | ||||||
|  |         url = 'https://prod.just-dance.com/sessions/v1/pairing-info' | ||||||
|  | 
 | ||||||
|  |         async with aiohttp.ClientSession(headers=self.headers) as session: | ||||||
|  |             async with session.get(url, params={'code': self.pairing_code}, ssl=False) as resp: | ||||||
|  |                 if resp.status != 200: | ||||||
|  |                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_INVALID_PAIRING_CODE) | ||||||
|  |                     raise Exception('ERROR: Invalid pairing code!') | ||||||
|  | 
 | ||||||
|  |                 json_body = await resp.json() | ||||||
|  |                 self.pairing_url = json_body['pairingUrl'].replace('https://', 'wss://') + 'smartphone' | ||||||
|  |                 self.requires_punch_pairing = json_body.get('requiresPunchPairing', False) | ||||||
|  | 
 | ||||||
|  |     async def send_initiate_punch_pairing(self): | ||||||
|  |         ''' Tell console which IP address & port to connect to ''' | ||||||
|  |         url = 'https://prod.just-dance.com/sessions/v1/initiate-punch-pairing' | ||||||
|  |         json_payload = { | ||||||
|  |             'pairingCode': self.pairing_code, | ||||||
|  |             'mobileIP': self.host_ip_addr, | ||||||
|  |             'mobilePort': self.host_port, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         async with aiohttp.ClientSession(headers=self.headers) as session: | ||||||
|  |             async with session.post(url, json=json_payload, ssl=False) as resp: | ||||||
|  |                 body = await resp.text() | ||||||
|  |                 if body != 'OK': | ||||||
|  |                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_PUNCH_PAIRING) | ||||||
|  |                     raise Exception('ERROR: Couldn\'t initiate punch pairing!') | ||||||
|  | 
 | ||||||
|  |     async def hole_punching(self): | ||||||
|  |         ''' Open a port on this machine so the console can connect to it ''' | ||||||
|  |         try: | ||||||
|  |             conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |             conn.bind(('0.0.0.0', self.host_port)) | ||||||
|  |             conn.listen(5) | ||||||
|  | 
 | ||||||
|  |             # Accept incoming connection from console | ||||||
|  |             console_conn, addr = conn.accept() | ||||||
|  |             self.console_conn = console_conn | ||||||
|  |             print('Connected with {}:{}'.format(addr[0], addr[1])) | ||||||
|  |         except Exception as e: | ||||||
|  |             await self.on_state_changed(self.joycon.serial, PairingState.ERROR_HOLE_PUNCHING) | ||||||
|  |             raise e | ||||||
|  | 
 | ||||||
|  |     async def send_message(self, __class, data={}): | ||||||
|  |         ''' Send JSON message to server ''' | ||||||
|  |         if __class != 'JD_PhoneScoringData': | ||||||
|  |             print('>>>', __class, data) | ||||||
|  | 
 | ||||||
|  |         msg = {'root': {'__class': __class}} | ||||||
|  |         if data: | ||||||
|  |             msg['root'].update(data) | ||||||
|  | 
 | ||||||
|  |         # Remove extra spaces from JSON to reduce size | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             await self.ws.send(json.dumps(msg, separators=(',', ':'))) | ||||||
|  |         except Exception: | ||||||
|  |             await self.disconnect(close_ws=False) | ||||||
|  | 
 | ||||||
|  |     async def on_message(self, message): | ||||||
|  |         message = json.loads(message) | ||||||
|  |         print('<<<', message) | ||||||
|  | 
 | ||||||
|  |         __class = message['__class'] | ||||||
|  |         if __class == 'JD_PhoneDataCmdHandshakeContinue': | ||||||
|  |             await self.send_message('JD_PhoneDataCmdSync', {'phoneID': message['phoneID']}) | ||||||
|  |         elif __class == 'JD_PhoneDataCmdSyncEnd': | ||||||
|  |             await self.send_message('JD_PhoneDataCmdSyncEnd', {'phoneID': message['phoneID']}) | ||||||
|  |             await self.on_state_changed(self.joycon.serial, PairingState.CONNECTED) | ||||||
|  |         elif __class == 'JD_EnableAccelValuesSending_ConsoleCommandData': | ||||||
|  |             self.should_start_accelerometer = True | ||||||
|  |             self.number_of_accels_sent = 0 | ||||||
|  |         elif __class == 'JD_DisableAccelValuesSending_ConsoleCommandData': | ||||||
|  |             self.should_start_accelerometer = False | ||||||
|  |         elif __class == 'InputSetup_ConsoleCommandData': | ||||||
|  |             if message.get('isEnabled', 0) == 1: | ||||||
|  |                 self.is_input_allowed = True | ||||||
|  |         elif __class == 'EnableCarousel_ConsoleCommandData': | ||||||
|  |             if message.get('isEnabled', 0) == 1: | ||||||
|  |                 self.is_input_allowed = True | ||||||
|  |         elif __class == 'JD_EnableLobbyStartbutton_ConsoleCommandData': | ||||||
|  |             if message.get('isEnabled', 0) == 1: | ||||||
|  |                 self.is_input_allowed = True | ||||||
|  |         elif __class == 'ShortcutSetup_ConsoleCommandData': | ||||||
|  |             if message.get('isEnabled', 0) == 1: | ||||||
|  |                 self.is_input_allowed = True | ||||||
|  |         elif __class == 'JD_PhoneUiShortcutData': | ||||||
|  |             shortcuts = set() | ||||||
|  |             for item in message.get('shortcuts', []): | ||||||
|  |                 if item['__class'] == 'JD_PhoneAction_Shortcut': | ||||||
|  |                     try: | ||||||
|  |                         shortcuts.add(Command(item['shortcutType'])) | ||||||
|  |                     except Exception as e: | ||||||
|  |                         print('Unknown Command: ', e) | ||||||
|  |             self.available_shortcuts = shortcuts | ||||||
|  |         elif __class == 'JD_OpenPhoneKeyboard_ConsoleCommandData': | ||||||
|  |             await asyncio.sleep(1) | ||||||
|  |             await self.send_message('JD_CancelKeyboard_PhoneCommandData') | ||||||
|  |         elif __class == 'JD_PhoneUiSetupData': | ||||||
|  |             self.is_input_allowed = True | ||||||
|  |             shortcuts = set() | ||||||
|  |             if message.get('setupData', {}).get('gameplaySetup', {}).get('pauseSlider', {}): | ||||||
|  |                 self.available_shortcuts.add(Command.PAUSE) | ||||||
|  | 
 | ||||||
|  |             if message['isPopup'] == 1: | ||||||
|  |                 self.is_input_allowed = True | ||||||
|  |             else: | ||||||
|  |                 self.is_input_allowed = (message.get('inputSetup', {}).get('isEnabled', 0) == 1) | ||||||
|  | 
 | ||||||
|  |     async def send_hello(self): | ||||||
|  |         print('Pairing...') | ||||||
|  | 
 | ||||||
|  |         await self.send_message('JD_PhoneDataCmdHandshakeHello', { | ||||||
|  |             'accelAcquisitionFreqHz': float(self.accel_acquisition_freq_hz), | ||||||
|  |             'accelAcquisitionLatency': float(self.accel_acquisition_latency), | ||||||
|  |             'accelMaxRange': float(self.accel_max_range), | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         async for message in self.ws: | ||||||
|  |             await self.on_message(message) | ||||||
|  | 
 | ||||||
|  |     async def send_accelerometer_data(self): | ||||||
|  |         accel_data = [] | ||||||
|  |         delta_time = 0 | ||||||
|  | 
 | ||||||
|  |         end = time.time() | ||||||
|  | 
 | ||||||
|  |         while True: | ||||||
|  |             if self.disconnected: | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             if not self.should_start_accelerometer: | ||||||
|  |                 await asyncio.sleep(0.5) | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             start = time.time() | ||||||
|  |             if delta_time > ACCEL_SEND_RATE: | ||||||
|  |                 delta_time = 0 | ||||||
|  |                 while len(accel_data) > 0: | ||||||
|  |                     accels_num = min(len(accel_data), 10) | ||||||
|  | 
 | ||||||
|  |                     await self.send_message('JD_PhoneScoringData', { | ||||||
|  |                         'accelData': accel_data[:accels_num], | ||||||
|  |                         'timeStamp': self.number_of_accels_sent, | ||||||
|  |                     }) | ||||||
|  | 
 | ||||||
|  |                     self.number_of_accels_sent += accels_num | ||||||
|  |                     accel_data = accel_data[accels_num:] | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 await asyncio.sleep(JOYCON_UPDATE_RATE) | ||||||
|  |                 joycon_status = self.joycon.get_status() | ||||||
|  |             except OSError: | ||||||
|  |                 self.disconnect() | ||||||
|  |                 return | ||||||
|  | 
 | ||||||
|  |             # Accelerator axes on phone & Joy-Con are different so we need to swap some axes | ||||||
|  |             # https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/imu_sensor_notes.md | ||||||
|  |             accel = joycon_status['accel'] | ||||||
|  |             x = accel['y'] * -1 | ||||||
|  |             y = accel['x'] | ||||||
|  |             z = accel['z'] | ||||||
|  | 
 | ||||||
|  |             accel_data.append([x, y, z]) | ||||||
|  | 
 | ||||||
|  |             end = time.time() | ||||||
|  |             delta_time += (end - start) * 1000 | ||||||
|  | 
 | ||||||
|  |     async def send_command(self): | ||||||
|  |         ''' Capture Joycon's input and send to console ''' | ||||||
|  |         while True: | ||||||
|  |             try: | ||||||
|  |                 if self.disconnected: | ||||||
|  |                     return | ||||||
|  | 
 | ||||||
|  |                 if not self.is_input_allowed and not self.should_start_accelerometer: | ||||||
|  |                     await asyncio.sleep(JOYCON_UPDATE_RATE * 5) | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |                 await asyncio.sleep(JOYCON_UPDATE_RATE * 5) | ||||||
|  | 
 | ||||||
|  |                 cmd = None | ||||||
|  |                 # Get pressed button | ||||||
|  |                 for event_type, status in self.joycon.events(): | ||||||
|  |                     if status == 0:  # 0 = pressed, 1 = released | ||||||
|  |                         continue | ||||||
|  | 
 | ||||||
|  |                     joycon_button = JoyConButton(event_type) | ||||||
|  |                     if self.should_start_accelerometer:  # Can only send Pause command while playing | ||||||
|  |                         if joycon_button == JoyConButton.PLUS or joycon_button == JoyConButton.MINUS: | ||||||
|  |                             cmd = Command.PAUSE | ||||||
|  |                     else: | ||||||
|  |                         if joycon_button == JoyConButton.A or joycon_button == JoyConButton.RIGHT: | ||||||
|  |                             cmd = Command.ACCEPT | ||||||
|  |                         elif joycon_button == JoyConButton.B or joycon_button == JoyConButton.DOWN: | ||||||
|  |                             cmd = Command.BACK | ||||||
|  |                         elif joycon_button in SHORTCUT_MAPPING: | ||||||
|  |                             # Get command depends on which button was pressed & which shortcuts are available | ||||||
|  |                             for shortcut in SHORTCUT_MAPPING[joycon_button]: | ||||||
|  |                                 if shortcut in self.available_shortcuts: | ||||||
|  |                                     cmd = shortcut | ||||||
|  |                                     break | ||||||
|  | 
 | ||||||
|  |                 # Get joystick direction | ||||||
|  |                 if not self.should_start_accelerometer and not cmd: | ||||||
|  |                     status = self.joycon.get_status() | ||||||
|  | 
 | ||||||
|  |                     # Check which Joycon (L/R) is being used | ||||||
|  |                     stick = status['analog-sticks']['left'] if self.joycon_is_left else status['analog-sticks']['right'] | ||||||
|  |                     vertical = stick['vertical'] | ||||||
|  |                     horizontal = stick['horizontal'] | ||||||
|  | 
 | ||||||
|  |                     if vertical < -0.5: | ||||||
|  |                         cmd = Command.DOWN | ||||||
|  |                     elif vertical > 0.5: | ||||||
|  |                         cmd = Command.UP | ||||||
|  |                     elif horizontal < -0.5: | ||||||
|  |                         cmd = Command.LEFT | ||||||
|  |                     elif horizontal > 0.5: | ||||||
|  |                         cmd = Command.RIGHT | ||||||
|  | 
 | ||||||
|  |                 # Send command to server | ||||||
|  |                 if cmd: | ||||||
|  |                     if cmd == Command.PAUSE: | ||||||
|  |                         __class = 'JD_Pause_PhoneCommandData' | ||||||
|  |                         data = {} | ||||||
|  |                     elif type(cmd.value) == str: | ||||||
|  |                         __class = 'JD_Custom_PhoneCommandData' | ||||||
|  |                         data = { | ||||||
|  |                             'identifier': cmd.value, | ||||||
|  |                         } | ||||||
|  |                     else: | ||||||
|  |                         __class = 'JD_Input_PhoneCommandData' | ||||||
|  |                         data = { | ||||||
|  |                             'input': cmd.value, | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                     # Only send input when it's allowed to, otherwise we might get a disconnection | ||||||
|  |                     if self.is_input_allowed: | ||||||
|  |                         await self.send_message(__class, data) | ||||||
|  |                         await asyncio.sleep(0.01) | ||||||
|  |             except Exception as e: | ||||||
|  |                 print(e) | ||||||
|  |                 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 | ||||||
|  | 
 | ||||||
|  |         server_hostname = self.console_conn.getpeername()[0] if self.console_conn else None | ||||||
|  |         try: | ||||||
|  |             async with websockets.connect( | ||||||
|  |                     self.pairing_url, | ||||||
|  |                     subprotocols=[WS_SUBPROTOCOL], | ||||||
|  |                     sock=self.console_conn, | ||||||
|  |                     ssl=ssl_context, | ||||||
|  |                     ping_timeout=None, | ||||||
|  |                     server_hostname=server_hostname | ||||||
|  |             ) as websocket: | ||||||
|  |                 try: | ||||||
|  |                     self.ws = websocket | ||||||
|  |                     await asyncio.gather( | ||||||
|  |                         self.send_hello(), | ||||||
|  |                         self.send_accelerometer_data(), | ||||||
|  |                         self.send_command(), | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |                 except websockets.ConnectionClosed: | ||||||
|  |                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) | ||||||
|  |                     await self.disconnect(close_ws=False) | ||||||
|  |         except Exception: | ||||||
|  |             await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) | ||||||
|  |             await self.disconnect(close_ws=False) | ||||||
|  | 
 | ||||||
|  |     async def disconnect(self, close_ws=True): | ||||||
|  |         print('disconnected') | ||||||
|  |         self.disconnected = True | ||||||
|  |         self.joycon.__del__() | ||||||
|  | 
 | ||||||
|  |         if close_ws and self.ws: | ||||||
|  |             await self.ws.close() | ||||||
|  | 
 | ||||||
|  |     async def pair(self): | ||||||
|  |         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) | ||||||
|  |             else: | ||||||
|  |                 await self.on_state_changed(self.joycon.serial, PairingState.GETTING_TOKEN) | ||||||
|  |                 print('Getting authorication token...') | ||||||
|  |                 await self.get_access_token() | ||||||
|  | 
 | ||||||
|  |                 await self.on_state_changed(self.joycon.serial, PairingState.PAIRING) | ||||||
|  |                 print('Sending pairing code...') | ||||||
|  |                 await self.send_pairing_code() | ||||||
|  | 
 | ||||||
|  |                 await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) | ||||||
|  |                 print('Connecting with console...') | ||||||
|  |                 if self.requires_punch_pairing: | ||||||
|  |                     await self.send_initiate_punch_pairing() | ||||||
|  |                     await self.hole_punching() | ||||||
|  | 
 | ||||||
|  |             await self.connect_ws() | ||||||
|  |         except Exception as e: | ||||||
|  |             await self.disconnect() | ||||||
|  |             print(e) | ||||||
							
								
								
									
										128
									
								
								joydance/constants.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								joydance/constants.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | ||||||
|  | from enum import Enum | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | JOYDANCE_VERSION = '0.1' | ||||||
|  | UBI_APP_ID = '210da0fb-d6a5-4ed1-9808-01e86f0de7fb' | ||||||
|  | UBI_SKU_ID = 'jdcompanion-android' | ||||||
|  | WS_SUBPROTOCOL = 'v2.phonescoring.jd.ubisoft.com' | ||||||
|  | 
 | ||||||
|  | JOYCON_UPDATE_RATE = 0.02  # 50Hz | ||||||
|  | ACCEL_SEND_RATE = 40  # ms | ||||||
|  | ACCEL_ACQUISITION_FREQ_HZ = 50  # Hz | ||||||
|  | ACCEL_ACQUISITION_LATENCY = 20  # ms | ||||||
|  | ACCEL_MAX_RANGE = 8  # ±G | ||||||
|  | 
 | ||||||
|  | DEFAULT_CONFIG = { | ||||||
|  |     'pairing_method': 'default', | ||||||
|  |     'host_ip_addr': '', | ||||||
|  |     'console_ip_addr': '', | ||||||
|  |     'pairing_code': '', | ||||||
|  |     'accel_acquisition_freq_hz': ACCEL_ACQUISITION_FREQ_HZ, | ||||||
|  |     'accel_acquisition_latency': ACCEL_ACQUISITION_LATENCY, | ||||||
|  |     'accel_max_range': ACCEL_MAX_RANGE, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Command(Enum): | ||||||
|  |     UP = 3690595578 | ||||||
|  |     RIGHT = 1099935642 | ||||||
|  |     DOWN = 2467711647 | ||||||
|  |     LEFT = 3652315484 | ||||||
|  |     ACCEPT = 1084313942 | ||||||
|  | 
 | ||||||
|  |     PAUSE = 'PAUSE' | ||||||
|  | 
 | ||||||
|  |     BACK = 'SHORTCUT_BACK' | ||||||
|  |     CHANGE_DANCERCARD = 'SHORTCUT_CHANGE_DANCERCARD' | ||||||
|  |     FAVORITE = 'SHORTCUT_FAVORITE' | ||||||
|  |     GOTO_SONGSTAB = 'SHORTCUT_GOTO_SONGSTAB' | ||||||
|  |     SKIP = 'SHORTCUT_SKIP' | ||||||
|  |     SORTING = 'SHORTCUT_SORTING' | ||||||
|  |     SWAP_GENDER = 'SHORTCUT_SWAP_GENDER' | ||||||
|  |     SWEAT_ACTIVATION = 'SHORTCUT_SWEAT_ACTIVATION' | ||||||
|  |     TOGGLE_COOP = 'SHORTCUT_TOGGLE_COOP' | ||||||
|  |     UPLAY = 'SHORTCUT_UPLAY' | ||||||
|  | 
 | ||||||
|  |     ACTIVATE_DANCERCARD = 'SHORTCUT_ACTIVATE_DANCERCARD' | ||||||
|  |     DELETE_DANCERCARD = 'SHORTCUT_DELETE_DANCERCARD' | ||||||
|  | 
 | ||||||
|  |     DELETE_PLAYLIST = 'SHORTCUT_DELETE_PLAYLIST' | ||||||
|  |     SAVE_PLAYLIST = 'SHORTCUT_SAVE_PLAYLIST' | ||||||
|  |     PLAYLIST_RENAME = 'SHORTCUT_PLAYLIST_RENAME' | ||||||
|  |     PLAYLIST_DELETE_SONG = 'SHORTCUT_PLAYLIST_DELETE_SONG' | ||||||
|  |     PLAYLIST_MOVE_SONG_LEFT = 'SHORTCUT_PLAYLIST_MOVE_SONG_LEFT' | ||||||
|  |     PLAYLIST_MOVE_SONG_RIGHT = 'SHORTCUT_PLAYLIST_MOVE_SONG_RIGHT' | ||||||
|  | 
 | ||||||
|  |     TIPS_NEXT = 'SHORTCUT_TIPS_NEXT' | ||||||
|  |     TIPS_PREVIOUS = 'SHORTCUT_TIPS_PREVIOUS' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class JoyConButton(Enum): | ||||||
|  |     # Joy-Con (L) | ||||||
|  |     UP = 'up' | ||||||
|  |     RIGHT = 'right' | ||||||
|  |     DOWN = 'down' | ||||||
|  |     LEFT = 'left' | ||||||
|  |     L = 'l' | ||||||
|  |     ZL = 'zl' | ||||||
|  |     MINUS = 'minus' | ||||||
|  |     CAPTURE = 'capture' | ||||||
|  |     LEFT_STICK = 'stick_l_btn' | ||||||
|  |     LEFT_SR = 'left_sr' | ||||||
|  |     LEFT_SL = 'left_sl' | ||||||
|  | 
 | ||||||
|  |     # Joy-Con (R) | ||||||
|  |     A = 'a' | ||||||
|  |     B = 'b' | ||||||
|  |     X = 'x' | ||||||
|  |     Y = 'y' | ||||||
|  |     R = 'r' | ||||||
|  |     ZR = 'zr' | ||||||
|  |     PLUS = 'plus' | ||||||
|  |     HOME = 'home' | ||||||
|  |     RIGHT_STICK = 'stick_r_btn' | ||||||
|  |     RIGHT_SL = 'right_sl' | ||||||
|  |     RIGHT_SR = 'right_sr' | ||||||
|  |     CHARGING_GRIP = 'charging-grip' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Assign buttons on Joy-Con (R) with commands | ||||||
|  | SHORTCUT_MAPPING = { | ||||||
|  |     JoyConButton.X: [ | ||||||
|  |         Command.DELETE_DANCERCARD, | ||||||
|  |         Command.DELETE_PLAYLIST, | ||||||
|  |         Command.GOTO_SONGSTAB, | ||||||
|  |         Command.PLAYLIST_DELETE_SONG, | ||||||
|  |         Command.SKIP, | ||||||
|  |         Command.SORTING, | ||||||
|  |         Command.SWAP_GENDER, | ||||||
|  |         Command.TOGGLE_COOP, | ||||||
|  |     ], | ||||||
|  |     JoyConButton.Y: [ | ||||||
|  |         Command.ACTIVATE_DANCERCARD, | ||||||
|  |         Command.CHANGE_DANCERCARD, | ||||||
|  |         Command.SWEAT_ACTIVATION, | ||||||
|  |         Command.UPLAY, | ||||||
|  |     ], | ||||||
|  |     JoyConButton.PLUS: [ | ||||||
|  |         Command.FAVORITE, | ||||||
|  |         Command.PAUSE, | ||||||
|  |         Command.PLAYLIST_RENAME, | ||||||
|  |         Command.SAVE_PLAYLIST, | ||||||
|  |     ], | ||||||
|  |     JoyConButton.R: [ | ||||||
|  |         Command.PLAYLIST_MOVE_SONG_LEFT, | ||||||
|  |         Command.TIPS_PREVIOUS, | ||||||
|  |     ], | ||||||
|  |     JoyConButton.ZR: [ | ||||||
|  |         Command.PLAYLIST_MOVE_SONG_RIGHT, | ||||||
|  |         Command.TIPS_NEXT, | ||||||
|  |     ], | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # Same with Joy-Con (L) | ||||||
|  | SHORTCUT_MAPPING[JoyConButton.UP] = JoyConButton.X | ||||||
|  | SHORTCUT_MAPPING[JoyConButton.LEFT] = JoyConButton.Y | ||||||
|  | SHORTCUT_MAPPING[JoyConButton.MINUS] = JoyConButton.PLUS | ||||||
|  | SHORTCUT_MAPPING[JoyConButton.L] = JoyConButton.R | ||||||
|  | SHORTCUT_MAPPING[JoyConButton.ZL] = JoyConButton.ZR | ||||||
							
								
								
									
										5
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | https://github.com/redphx/joycon-python/archive/refs/heads/master.zip#egg=joycon-python | ||||||
|  | websockets==10.2 | ||||||
|  | aiohttp==3.8.1 | ||||||
|  | hidapi==0.11.2 | ||||||
|  | pyglm==2.5.7 | ||||||
							
								
								
									
										180
									
								
								static/css/app.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								static/css/app.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,180 @@ | ||||||
|  | body { | ||||||
|  |     max-width: 500px; | ||||||
|  |     margin: 10px auto; | ||||||
|  |     background-color: #1c1b1f; | ||||||
|  |     color: #e1e1e1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .container { | ||||||
|  |     border-radius: 10px; | ||||||
|  |     background-color: #272629; | ||||||
|  |     padding: 20px; | ||||||
|  |     margin: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ascii { | ||||||
|  |     text-align: center; | ||||||
|  |     padding: 8px 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ascii pre { | ||||||
|  |     font-family: ui-monospace, 'Segoe UI Mono', 'Roboto Mono', 'Courier New', monospace; | ||||||
|  |     display: inline-block; | ||||||
|  |     text-align: left; | ||||||
|  |     margin: 0; | ||||||
|  |     font-weight: bold; | ||||||
|  |     font-size: 10px; | ||||||
|  |     line-height: 10px; | ||||||
|  |     color: #FFF; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .flex { | ||||||
|  |     display: flex; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons { | ||||||
|  |     margin-top: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons-wrapper button { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper { | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .empty { | ||||||
|  |     height: 60px; | ||||||
|  |     line-height: 60px; | ||||||
|  |     margin: auto; | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list { | ||||||
|  |     padding: 0; | ||||||
|  |     margin: 0; | ||||||
|  |     list-style: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list li { | ||||||
|  |     border-bottom: 1px solid #2f2f2f; | ||||||
|  |     padding: 12px 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list .joycon-color { | ||||||
|  |     width: 35%; | ||||||
|  |     margin: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list .joycon-name { | ||||||
|  |     align-items: center; | ||||||
|  |     font-size: 16px; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list .joycon-info { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .joycons-wrapper .joycons-list .joycon-info .joycon-state { | ||||||
|  |     color: #757575; | ||||||
|  |     font-size: 14px; | ||||||
|  |     text-align: left; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons-list .pairing-code { | ||||||
|  |     border-radius: 4px; | ||||||
|  |     font-size: 12px; | ||||||
|  |     background-color: #202020; | ||||||
|  |     display: inline-block; | ||||||
|  |     color: #a6a6a6; | ||||||
|  |     padding: 4px; | ||||||
|  |     margin: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .joycons .btn-refresh { | ||||||
|  |     color: #383000; | ||||||
|  |     background-color: #e2c700; | ||||||
|  |     margin: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input, select { | ||||||
|  |     box-shadow: none !important; | ||||||
|  |     border-width: 2px !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input:invalid { | ||||||
|  |     border-color: #e9322d !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input[readonly] { | ||||||
|  |     background-color: #ccc !important; | ||||||
|  |     color: #000; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #ipAddr, #pairingCode, .joycons-list .pairing-code { | ||||||
|  |     font-family: ui-monospace, 'Segoe UI Mono', 'Roboto Mono', 'Courier New', monospace; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .footer { | ||||||
|  |     text-align: center; | ||||||
|  |     margin-top: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .footer a { | ||||||
|  |     text-decoration: none; | ||||||
|  |     color: #424242; | ||||||
|  |     font-size: 14px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .footer a:hover { | ||||||
|  |     color: #4d4d4d; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .pure-button-primary { | ||||||
|  |     background-color: #1976D2 !important; | ||||||
|  |     color: #E3F2FD !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .pure-form fieldset { | ||||||
|  |     padding-bottom: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level { | ||||||
|  |     display: inline-block; | ||||||
|  |     margin-left: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level svg { | ||||||
|  |     width: 16px; | ||||||
|  |     margin: auto; | ||||||
|  |     vertical-align: middle; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level.full > * { | ||||||
|  |     fill: #8BC34A; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level.medium > * { | ||||||
|  |     fill: #FFEB3B; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level.low > * { | ||||||
|  |     fill: #FF9800; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level.critical > * { | ||||||
|  |     fill: #FF5722; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .battery-level.medium .battery-bar-4, | ||||||
|  | .battery-level.low .battery-bar-3, | ||||||
|  | .battery-level.low .battery-bar-4, | ||||||
|  | .battery-level.critical .battery-bar-2, | ||||||
|  | .battery-level.critical .battery-bar-3, | ||||||
|  | .battery-level.critical .battery-bar-4 { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								static/css/grids-responsive-min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								static/css/grids-responsive-min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										11
									
								
								static/css/pure-min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								static/css/pure-min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/favicon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/favicon.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 470 B | 
							
								
								
									
										17
									
								
								static/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								static/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>JoyDance</title> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <link rel="icon" type="image/png" href="/favicon.png"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||||
|  |     <link href="/css/pure-min.css" type="text/css" rel="stylesheet" /> | ||||||
|  |     <link href="/css/grids-responsive-min.css" type="text/css" rel="stylesheet" /> | ||||||
|  |     <link href="/css/app.css" type="text/css" rel="stylesheet" /> | ||||||
|  |     <script type="text/javascript">window.CONFIG = [[CONFIG]]; window.VERSION = '[[VERSION]]';</script> | ||||||
|  |     <script type="text/javascript" src="/js/mitt.umd.js"></script> | ||||||
|  |     <script type="module" src="/js/app.js"></script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										507
									
								
								static/js/app.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										507
									
								
								static/js/app.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,507 @@ | ||||||
|  | import { h, Component, render } from '/js/preact.module.js'; | ||||||
|  | import htm from '/js/htm.module.js'; | ||||||
|  | 
 | ||||||
|  | // Initialize htm with Preact
 | ||||||
|  | const html = htm.bind(h); | ||||||
|  | window.mitty = mitt() | ||||||
|  | 
 | ||||||
|  | const SVG_BATTERY_LEVEL = html`<svg style="enable-background:new 0 0 16 16" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M15 4H0v8h15V9h1V7h-1V4zm-1 3v4H1V5h13v2z"/><rect class="battery-bar-4" height="4" width="2" x="11" y="6"/><rect class="battery-bar-3" height="4" width="2" x="8" y="6"/><rect class="battery-bar-2" height="4" width="2" x="5" y="6"/><rect class="battery-bar-1" height="4" width="2" x="2" y="6"/></svg>` | ||||||
|  | 
 | ||||||
|  | const BATTERY_LEVEL = { | ||||||
|  |     4: 'full', | ||||||
|  |     3: 'medium', | ||||||
|  |     2: 'low', | ||||||
|  |     1: 'critical', | ||||||
|  |     0: 'critical', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const PairingMethod = { | ||||||
|  |     DEFAULT: 'default', | ||||||
|  |     FAST: 'fast' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const WsCommand = { | ||||||
|  |     GET_JOYCON_LIST: 'get_joycon_list', | ||||||
|  |     CONNECT_JOYCON: 'connect_joycon', | ||||||
|  |     DISCONNECT_JOYCON: 'disconnect_joycon', | ||||||
|  |     UPDATE_JOYCON_STATE: 'update_joycon_state', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const PairingState = { | ||||||
|  |     IDLE: 0, | ||||||
|  |     GETTING_TOKEN: 1, | ||||||
|  |     PAIRING: 2, | ||||||
|  |     CONNECTING: 3, | ||||||
|  |     CONNECTED: 4, | ||||||
|  |     DISCONNECTING: 5, | ||||||
|  |     DISCONNECTED: 10, | ||||||
|  | 
 | ||||||
|  |     ERROR_JOYCON: 101, | ||||||
|  |     ERROR_CONNECTION: 102, | ||||||
|  |     ERROR_INVALID_PAIRING_CODE: 103, | ||||||
|  |     ERROR_PUNCH_PAIRING: 104, | ||||||
|  |     ERROR_HOLE_PUNCHING: 105, | ||||||
|  |     ERROR_CONSOLE_CONNECTION: 106, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const PairingStateMessage = { | ||||||
|  |     [PairingState.IDLE]: 'Idle', | ||||||
|  |     [PairingState.GETTING_TOKEN]: 'Getting auth token...', | ||||||
|  |     [PairingState.PAIRING]: 'Sending pairing code...', | ||||||
|  |     [PairingState.CONNECTING]: 'Connecting with console...', | ||||||
|  |     [PairingState.CONNECTED]: 'Connected!', | ||||||
|  |     [PairingState.DISCONNECTED]: 'Disconnected', | ||||||
|  | 
 | ||||||
|  |     [PairingState.ERROR_JOYCON]: 'Joy-Con problem!', | ||||||
|  |     [PairingState.ERROR_CONNECTION]: 'Couldn\'t get auth token!', | ||||||
|  |     [PairingState.ERROR_INVALID_PAIRING_CODE]: 'Invalid pairing code!', | ||||||
|  |     [PairingState.ERROR_PUNCH_PAIRING]: 'Couldn\'t punch pairing!', | ||||||
|  |     [PairingState.ERROR_HOLE_PUNCHING]: 'Couldn\'t connect with console!', | ||||||
|  |     [PairingState.ERROR_CONSOLE_CONNECTION]: 'Couldn\'t connect with console!', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PairingMethodPicker extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super() | ||||||
|  |         this.state = { | ||||||
|  |             pairing_method: props.pairing_method, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.onChange = this.onChange.bind(this) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onChange(e) { | ||||||
|  |         const pairing_method = e.target.value | ||||||
|  |         this.setState({ | ||||||
|  |             pairing_method: pairing_method, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         window.mitty.emit('update_method', pairing_method) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props) { | ||||||
|  |         return html` | ||||||
|  |             <label for="stacked-state">Pairing Method</label> | ||||||
|  |             <select id="stacked-state" onChange=${this.onChange} value=${props.pairing_method}> | ||||||
|  |                 <option value="${PairingMethod.DEFAULT}">Default: All platforms (incl. Xbox Series/Stadia)</option> | ||||||
|  |                 <option value="${PairingMethod.FAST}">Fast: Xbox One/PlayStation/Nintendo Switch</option> | ||||||
|  |             </select> | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PrivateIpAddress extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props) | ||||||
|  | 
 | ||||||
|  |         let lock_host = false | ||||||
|  |         let host_ip_addr = props.host_ip_addr | ||||||
|  |         let console_ip_addr = props.console_ip_addr | ||||||
|  | 
 | ||||||
|  |         let hostname = window.location.hostname | ||||||
|  |         if (hostname.startsWith('192.168.')) { | ||||||
|  |             host_ip_addr = hostname | ||||||
|  |             lock_host = true | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.state = { | ||||||
|  |             host_ip_addr: host_ip_addr, | ||||||
|  |             console_ip_addr: console_ip_addr, | ||||||
|  |             lock_host: lock_host, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.onKeyPress = this.onKeyPress.bind(this) | ||||||
|  |         this.onChange = this.onChange.bind(this) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onChange(e) { | ||||||
|  |         const key = this.props.pairing_method == PairingMethod.DEFAULT ? 'host_ip_addr' : 'console_ip_addr' | ||||||
|  |         const value = e.target.value | ||||||
|  |         this.setState({ | ||||||
|  |             [key]: value, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         window.mitty.emit('update_addr', value) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onKeyPress(e) { | ||||||
|  |         if (!/[0-9\.]/.test(e.key)) { | ||||||
|  |             e.preventDefault() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     componentDidMount() { | ||||||
|  |         window.mitty.emit('update_addr', this.state.host_ip_addr) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props, state) { | ||||||
|  |         const pairing_method = props.pairing_method | ||||||
|  |         const addr = pairing_method == PairingMethod.DEFAULT ? state.host_ip_addr : state.console_ip_addr | ||||||
|  |         return html` | ||||||
|  |             <label> | ||||||
|  |                 ${pairing_method == PairingMethod.DEFAULT && html`Host's Private IP Address`} | ||||||
|  |                 ${pairing_method == PairingMethod.FAST && html`Console's Private IP Address`} | ||||||
|  |             </label> | ||||||
|  | 
 | ||||||
|  |             ${pairing_method == PairingMethod.DEFAULT && state.lock_host && html` | ||||||
|  |                 <input readonly required id="ipAddr" type="text" size="15" placeholder="${addr}" /> | ||||||
|  |             `}
 | ||||||
|  | 
 | ||||||
|  |             ${(pairing_method == PairingMethod.FAST || !state.lock_host) && html` | ||||||
|  |                 <input required id="ipAddr" type="text" inputmode="decimal" size="15" maxlength="15" placeholder="192.168.x.x" pattern="^192\\.168\\.((\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.)(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])$" value=${addr} onKeyPress=${this.onKeyPress} onChange="${this.onChange}" /> | ||||||
|  |             `}
 | ||||||
|  | 
 | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PairingCode extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props) | ||||||
|  |         this.state = { | ||||||
|  |             pairing_code: props.pairing_code, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.onChange = this.onChange.bind(this) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onChange(e) { | ||||||
|  |         const value = e.target.value | ||||||
|  |         this.setState({ | ||||||
|  |             pairing_code: value, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         window.mitty.emit('update_code', value) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props, state) { | ||||||
|  |         return html` | ||||||
|  |             <label>Pairing Code</label> | ||||||
|  |             ${props.pairing_method == PairingMethod.DEFAULT && html` | ||||||
|  |                 <input required id="pairingCode" type="text" inputmode="decimal" value=${state.pairing_code} placeholder="000000" maxlength="6" size="6" pattern="[0-9]{6}" onKeyPress=${(e) => !/[0-9]/.test(e.key) && e.preventDefault()} onChange=${this.onChange} /> | ||||||
|  |             `}
 | ||||||
|  |             ${props.pairing_method == PairingMethod.FAST && html` | ||||||
|  |                 <input type="text" id="pairingCode" value="" readonly placeholder="Not Required" size="12" /> | ||||||
|  |             `}
 | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class JoyCon extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props) | ||||||
|  | 
 | ||||||
|  |         this.connect = this.connect.bind(this) | ||||||
|  |         this.disconnect = this.disconnect.bind(this) | ||||||
|  |         this.onStateUpdated = this.onStateUpdated.bind(this) | ||||||
|  | 
 | ||||||
|  |         this.state = { | ||||||
|  |             ...props.joycon, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         window.mitty.on('resp_' + WsCommand.DISCONNECT_JOYCON, this.onDisconnected) | ||||||
|  |         window.mitty.on('resp_' + WsCommand.UPDATE_JOYCON_STATE, this.onStateUpdated) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     connect() { | ||||||
|  |         window.mitty.emit('req_' + WsCommand.CONNECT_JOYCON, this.props.joycon.serial) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     disconnect() { | ||||||
|  |         window.mitty.emit('req_' + WsCommand.DISCONNECT_JOYCON, this.props.joycon.serial) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onStateUpdated(data) { | ||||||
|  |         if (data['serial'] != this.props.joycon.serial) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const state = data['state'] | ||||||
|  |         if (PairingStateMessage.hasOwnProperty(state)) { | ||||||
|  |             this.setState({ | ||||||
|  |                 ...data, | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props, { name, state, pairing_code, is_left, color, battery_level }) { | ||||||
|  |         const joyconState = state | ||||||
|  |         const stateMessage = PairingStateMessage[joyconState] | ||||||
|  |         let showButton = true | ||||||
|  |         if ([PairingState.GETTING_TOKEN, PairingState.PAIRING, PairingState.CONNECTING].indexOf(joyconState) > -1) { | ||||||
|  |             showButton = false | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let joyconSvg | ||||||
|  |         if (is_left) { | ||||||
|  |             joyconSvg = html`<svg class="joycon-color" viewBox="0 0 171 453" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd"><path d="M219.594 33.518v412.688c0 1.023-.506 1.797-1.797 1.797h-49.64c-51.68 0-85.075-45.698-85.075-85.075V114.987c0-57.885 56.764-84.719 84.719-84.719h48.79c2.486 0 3.003 1.368 3.003 3.25zm-32.123 105.087c0 17.589-14.474 32.062-32.063 32.062-17.589 0-32.062-14.473-32.062-32.062s14.473-32.063 32.062-32.063 32.063 14.474 32.063 32.063z" style="fill:${color};stroke:#000;stroke-width:8.33px" transform="translate(-65.902 -13.089)"/></svg>` | ||||||
|  |         } else { | ||||||
|  |             joyconSvg = html`<svg class="joycon-color" viewBox="0 0 171 453" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd"><path d="M324.763 40.363v412.688c0 1.023.506 1.797 1.797 1.797h49.64c51.68 0 85.075-45.698 85.075-85.075V121.832c0-6.774-.777-13.123-2.195-19.054-10.696-44.744-57.841-65.665-82.524-65.665h-48.79c-2.486 0-3.003 1.368-3.003 3.25zm96 218.094c0 17.589-14.473 32.063-32.062 32.063s-32.063-14.474-32.063-32.063c0-17.589 14.474-32.062 32.063-32.062 17.589 0 32.062 14.473 32.062 32.062z" style="fill:${color};fill-rule:nonzero;stroke:#000;stroke-width:8.33px" transform="translate(-307.583 -19.934)"/></svg>` | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const batteryLevel = BATTERY_LEVEL[battery_level] | ||||||
|  | 
 | ||||||
|  |         return html` | ||||||
|  |             <li> | ||||||
|  |                 <div class="pure-g"> | ||||||
|  | 
 | ||||||
|  |                     <div class="pure-u-2-24 flex">${joyconSvg}</div> | ||||||
|  |                     <div class="pure-u-12-24 joycon-info"> | ||||||
|  |                         <div class="flex"> | ||||||
|  |                             <span class="joycon-name">${name}</span> | ||||||
|  |                             <span class="battery-level ${batteryLevel}">${SVG_BATTERY_LEVEL}</span> | ||||||
|  |                         </div> | ||||||
|  |                         <span class="joycon-state">${stateMessage}</span> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pure-u-4-24 flex"> | ||||||
|  |                         ${pairing_code && html` | ||||||
|  |                             <span class="pairing-code">${pairing_code}</span> | ||||||
|  |                         `}
 | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pure-u-6-24"> | ||||||
|  |                         ${showButton && joyconState == PairingState.CONNECTED && html` | ||||||
|  |                             <button type="button" onClick=${this.disconnect} class="pure-button pure-button-error">Disconnect</button> | ||||||
|  |                         `}
 | ||||||
|  |                         ${showButton && joyconState != PairingState.CONNECTED && html` | ||||||
|  |                             <button type="button" onClick=${this.connect} class="pure-button pure-button-primary">Connect</button> | ||||||
|  |                         `}
 | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </li> | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class JoyCons extends Component { | ||||||
|  |     constructor() { | ||||||
|  |         super() | ||||||
|  |         this.state = { | ||||||
|  |             isRefreshing: false, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.refreshJoyconList = this.refreshJoyconList.bind(this) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     refreshJoyconList() { | ||||||
|  |         this.setState({ | ||||||
|  |             isRefreshing: false, | ||||||
|  |         }) | ||||||
|  |         window.mitty.emit('req_' + WsCommand.GET_JOYCON_LIST) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     componentDidMount() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props, state) { | ||||||
|  |         return html` | ||||||
|  |             <div class="pure-g"> | ||||||
|  |                 <h2 class="pure-u-18-24">Joy-Cons</h2> | ||||||
|  |                     ${state.isRefreshing && html` | ||||||
|  |                         <button type="button" disabled class="pure-button btn-refresh pure-u-6-24">Refresh</a> | ||||||
|  |                     `}
 | ||||||
|  |                     ${!state.isRefreshing && html` | ||||||
|  |                         <button type="button" class="pure-button btn-refresh pure-u-6-24" onClick=${this.refreshJoyconList}>Refresh</button> | ||||||
|  |                     `}
 | ||||||
|  |             </div> | ||||||
|  |             <div class="joycons-wrapper"> | ||||||
|  |                 ${props.joycons.length == 0 && html` | ||||||
|  |                     <p class="empty">No Joy-Cons found!</p> | ||||||
|  |                 `}
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 ${props.joycons.length > 0 && html` | ||||||
|  |                     <ul class="joycons-list"> | ||||||
|  |                         ${props.joycons.map(item => ( | ||||||
|  |                             html`<${JoyCon} joycon=${item} key=${item.serial} />` | ||||||
|  |                         ))} | ||||||
|  |                     </ul> | ||||||
|  |                 `}
 | ||||||
|  |             </div> | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class App extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super() | ||||||
|  | 
 | ||||||
|  |         this.state = { | ||||||
|  |             pairing_method: window.CONFIG.pairing_method, | ||||||
|  |             host_ip_addr: window.CONFIG.host_ip_addr, | ||||||
|  |             console_ip_addr: window.CONFIG.console_ip_addr, | ||||||
|  |             pairing_code: window.CONFIG.pairing_code, | ||||||
|  |             joycons: [], | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.connectWs = this.connectWs.bind(this) | ||||||
|  |         this.sendRequest = this.sendRequest.bind(this) | ||||||
|  |         this.requestGetJoyconList = this.requestGetJoyconList.bind(this) | ||||||
|  |         this.requestConnectJoycon = this.requestConnectJoycon.bind(this) | ||||||
|  |         this.requestDisconnectJoycon = this.requestDisconnectJoycon.bind(this) | ||||||
|  |         this.handleMethodChange = this.handleMethodChange.bind(this) | ||||||
|  |         this.handleAddrChange = this.handleAddrChange.bind(this) | ||||||
|  |         this.handleCodeChange = this.handleCodeChange.bind(this) | ||||||
|  | 
 | ||||||
|  |         window.mitty.on('req_' + WsCommand.GET_JOYCON_LIST, this.requestGetJoyconList) | ||||||
|  |         window.mitty.on('req_' + WsCommand.CONNECT_JOYCON, this.requestConnectJoycon) | ||||||
|  |         window.mitty.on('req_' + WsCommand.DISCONNECT_JOYCON, this.requestDisconnectJoycon) | ||||||
|  |         window.mitty.on('update_method', this.handleMethodChange) | ||||||
|  |         window.mitty.on('update_addr', this.handleAddrChange) | ||||||
|  |         window.mitty.on('update_code', this.handleCodeChange) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     sendRequest(cmd, data) { | ||||||
|  |         if (!data) { | ||||||
|  |             data = {} | ||||||
|  |         } | ||||||
|  |         const msg = { | ||||||
|  |             cmd: cmd, | ||||||
|  |             data: data, | ||||||
|  |         } | ||||||
|  |         console.log('send', msg) | ||||||
|  |         this.socket.send(JSON.stringify(msg)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     requestGetJoyconList() { | ||||||
|  |         this.sendRequest(WsCommand.GET_JOYCON_LIST); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     requestConnectJoycon(serial) { | ||||||
|  |         const state = this.state | ||||||
|  |         const pairing_method = state.pairing_method | ||||||
|  |         let addr = pairing_method == PairingMethod.DEFAULT ? state.host_ip_addr : state.console_ip_addr | ||||||
|  |         if (!addr.match(/^192\.168\.((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.)(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/)) { | ||||||
|  |             alert('ERROR: Invalid IP address!') | ||||||
|  |             document.getElementById('ipAddr').focus() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (pairing_method == PairingMethod.DEFAULT) { | ||||||
|  |             const pairing_code = state.pairing_code | ||||||
|  |             if (!pairing_code.match(/^\d{6}$/)) { | ||||||
|  |                 alert('ERROR: Invalid pairing code!') | ||||||
|  |                 document.getElementById('pairingCode').focus() | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.sendRequest(WsCommand.CONNECT_JOYCON, { | ||||||
|  |             pairing_method: state.pairing_method, | ||||||
|  |             host_ip_addr: state.host_ip_addr, | ||||||
|  |             console_ip_addr: state.console_ip_addr, | ||||||
|  |             pairing_code: state.pairing_code, | ||||||
|  |             joycon_serial: serial, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     requestDisconnectJoycon(serial) { | ||||||
|  |         this.sendRequest(WsCommand.DISCONNECT_JOYCON, { | ||||||
|  |             joycon_serial: serial, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     connectWs() { | ||||||
|  |         const that = this | ||||||
|  |         this.socket = new WebSocket('ws://' + window.location.host + '/ws') | ||||||
|  | 
 | ||||||
|  |         this.socket.onopen = function(e) { | ||||||
|  |             console.log('[open] Connection established') | ||||||
|  |             that.requestGetJoyconList() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.socket.onmessage = function(event) { | ||||||
|  |             const msg = JSON.parse(event.data) | ||||||
|  |             console.log(msg) | ||||||
|  |             const cmd = msg['cmd'] | ||||||
|  |             const shortCmd = msg['cmd'].slice(5)  // Remove "resp_" prefix
 | ||||||
|  | 
 | ||||||
|  |             switch (shortCmd) { | ||||||
|  |                 case WsCommand.GET_JOYCON_LIST: | ||||||
|  |                     that.setState({ | ||||||
|  |                         joycons: msg['data'], | ||||||
|  |                     }) | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |                 default: | ||||||
|  |                     window.mitty.emit(cmd, msg['data']) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.socket.onclose = function(event) { | ||||||
|  |             if (event.wasClean) { | ||||||
|  |                 console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); | ||||||
|  |             } else { | ||||||
|  |                 console.log('[close] Connection died'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.socket.onerror = function(error) { | ||||||
|  |             console.log(`[error] ${error.message}`); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     handleMethodChange(pairing_method) { | ||||||
|  |         this.setState({ | ||||||
|  |             pairing_method: pairing_method, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     handleAddrChange(addr) { | ||||||
|  |         const key = this.state.pairing_method == PairingMethod.DEFAULT ? 'host_ip_addr' : 'console_ip_addr' | ||||||
|  |         this.setState({ | ||||||
|  |             [key]: addr, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     handleCodeChange(pairing_code) { | ||||||
|  |         this.setState({ | ||||||
|  |             pairing_code: pairing_code, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     componentDidMount() { | ||||||
|  |         this.connectWs() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     render(props, state) { | ||||||
|  |         return html` | ||||||
|  |             <div class="container"> | ||||||
|  |                 <div class="ascii"> | ||||||
|  |                     <pre>     ░░  ░░░░░░  ░░    ░░ ░░░░░░   ░░░░░  ░░░    ░░  ░░░░░░ ░░░░░░░ | ||||||
|  |      ▒▒ ▒▒    ▒▒  ▒▒  ▒▒  ▒▒   ▒▒ ▒▒   ▒▒ ▒▒▒▒   ▒▒ ▒▒      ▒▒ | ||||||
|  |      ▒▒ ▒▒    ▒▒   ▒▒▒▒   ▒▒   ▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒  ▒▒ ▒▒      ▒▒▒▒▒ | ||||||
|  | ▓▓   ▓▓ ▓▓    ▓▓    ▓▓    ▓▓   ▓▓ ▓▓   ▓▓ ▓▓  ▓▓ ▓▓ ▓▓      ▓▓ | ||||||
|  |  █████   ██████     ██    ██████  ██   ██ ██   ████  ██████ ███████ | ||||||
|  |                     </pre> | ||||||
|  |                 </div> | ||||||
|  | 
 | ||||||
|  |                 <form class="pure-form pure-form-stacked"> | ||||||
|  |                     <fieldset> | ||||||
|  |                         <div class="pure-g"> | ||||||
|  |                             <h2 class="pure-u-1">Config</h2> | ||||||
|  |                             <div class="pure-u-1"> | ||||||
|  |                                 <${PairingMethodPicker} pairing_method=${state.pairing_method}/> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="pure-u-1-2"> | ||||||
|  |                                 <${PrivateIpAddress} pairing_method=${state.pairing_method} host_ip_addr=${state.host_ip_addr} console_ip_addr=${state.console_ip_addr} /> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="pure-u-1-2"> | ||||||
|  |                                 <${PairingCode} pairing_method=${state.pairing_method} pairing_code=${state.pairing_code} /> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </fieldset> | ||||||
|  |                 </form> | ||||||
|  | 
 | ||||||
|  |                 <div class="pure-u-1 joycons"> | ||||||
|  |                     <${JoyCons} pairing_method=${state.pairing_method} joycons=${state.joycons} /> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="footer"> | ||||||
|  |                 <a href="https://github.com/redphx/joydance" target="_blank">${window.VERSION}</a> | ||||||
|  |             </div> | ||||||
|  |         ` | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | render(html`<${App} />`, document.body) | ||||||
							
								
								
									
										1
									
								
								static/js/htm.module.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/js/htm.module.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | var n = function (t, s, r, e) {var u;s[0] = 0;for (var h = 1; h < s.length; h++) {var p = s[h++],a = s[h] ? (s[0] |= p ? 1 : 2, r[s[h++]]) : s[++h];3 === p ? e[0] = a : 4 === p ? e[1] = Object.assign(e[1] || {}, a) : 5 === p ? (e[1] = e[1] || {})[s[++h]] = a : 6 === p ? e[1][s[++h]] += a + "" : p ? (u = t.apply(a, n(t, a, r, ["", null])), e.push(u), a[0] ? s[0] |= 2 : (s[h - 2] = 0, s[h] = u)) : e.push(a);}return e;},t = new Map();export default function (s) {var r = t.get(this);return r || (r = new Map(), t.set(this, r)), (r = n(this, r.get(s) || (r.set(s, r = function (n) {for (var t, s, r = 1, e = "", u = "", h = [0], p = function (n) {1 === r && (n || (e = e.replace(/^\s*\n\s*|\s*\n\s*$/g, ""))) ? h.push(0, n, e) : 3 === r && (n || e) ? (h.push(3, n, e), r = 2) : 2 === r && "..." === e && n ? h.push(4, n, 0) : 2 === r && e && !n ? h.push(5, 0, !0, e) : r >= 5 && ((e || !n && 5 === r) && (h.push(r, 0, e, s), r = 6), n && (h.push(r, n, 0, s), r = 6)), e = "";}, a = 0; a < n.length; a++) {a && (1 === r && p(), p(a));for (var l = 0; l < n[a].length; l++) t = n[a][l], 1 === r ? "<" === t ? (p(), h = [h], r = 3) : e += t : 4 === r ? "--" === e && ">" === t ? (r = 1, e = "") : e = t + e[0] : u ? t === u ? u = "" : e += t : '"' === t || "'" === t ? u = t : ">" === t ? (p(), r = 1) : r && ("=" === t ? (r = 5, s = e, e = "") : "/" === t && (r < 5 || ">" === n[a][l + 1]) ? (p(), 3 === r && (h = h[0]), r = h, (h = h[0]).push(2, 0, r), r = 0) : " " === t || "\t" === t || "\n" === t || "\r" === t ? (p(), r = 2) : e += t), 3 === r && "!--" === e && (r = 4, h = h[0]);}return p(), h;}(s)), r), arguments, [])).length > 1 ? r : r[0];} | ||||||
							
								
								
									
										1
									
								
								static/js/mitt.umd.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/js/mitt.umd.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f?f.push(t):e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&(t?f.splice(f.indexOf(t)>>>0,1):e.set(n,[]))},emit:function(n,t){var f=e.get(n);f&&f.slice().map(function(e){e(t)}),(f=e.get("*"))&&f.slice().map(function(e){e(n,t)})}}}}); | ||||||
							
								
								
									
										1
									
								
								static/js/preact.module.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/js/preact.module.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue