First public release
This commit is contained in:
parent
963cae3756
commit
64333fdb4a
|
@ -0,0 +1,3 @@
|
|||
.vscode/
|
||||
__pycache__/
|
||||
config.cfg
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 470 B |
|
@ -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>
|
|
@ -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)
|
|
@ -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];}
|
|
@ -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)})}}}});
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue