Skip to content

Commit 64333fd

Browse files
committed
First public release
1 parent 963cae3 commit 64333fd

14 files changed

Lines changed: 1580 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode/
2+
__pycache__/
3+
config.cfg

dance.py

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import asyncio
2+
import json
3+
import logging
4+
import re
5+
import socket
6+
import time
7+
from configparser import ConfigParser
8+
from enum import Enum
9+
10+
import hid
11+
from aiohttp import WSMsgType, web
12+
from pyjoycon import ButtonEventJoyCon, JoyCon
13+
from pyjoycon.constants import JOYCON_PRODUCT_IDS, JOYCON_VENDOR_ID
14+
15+
from joydance import JoyDance, PairingState
16+
from joydance.constants import DEFAULT_CONFIG, JOYDANCE_VERSION
17+
18+
logging.getLogger('asyncio').setLevel(logging.WARNING)
19+
20+
21+
class WsCommand(Enum):
22+
GET_JOYCON_LIST = 'get_joycon_list'
23+
CONNECT_JOYCON = 'connect_joycon'
24+
DISCONNECT_JOYCON = 'disconnect_joycon'
25+
UPDATE_JOYCON_STATE = 'update_joycon_state'
26+
27+
28+
class PairingMethod(Enum):
29+
DEFAULT = 'default'
30+
FAST = 'fast'
31+
32+
33+
REGEX_PAIRING_CODE = re.compile(r'^\d{6}$')
34+
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])$')
35+
36+
37+
async def get_device_ids():
38+
devices = hid.enumerate(JOYCON_VENDOR_ID, 0)
39+
40+
out = []
41+
for device in devices:
42+
vendor_id = device['vendor_id']
43+
product_id = device['product_id']
44+
product_string = device['product_string']
45+
serial = device.get('serial') or device.get('serial_number')
46+
47+
if product_id not in JOYCON_PRODUCT_IDS:
48+
continue
49+
50+
if not product_string:
51+
continue
52+
53+
out.append({
54+
'vendor_id': vendor_id,
55+
'product_id': product_id,
56+
'serial': serial,
57+
'product_string': product_string,
58+
})
59+
60+
return out
61+
62+
63+
async def get_joycon_list(app):
64+
joycons = []
65+
devices = await get_device_ids()
66+
67+
for dev in devices:
68+
if dev['serial'] in app['joycons_info']:
69+
info = app['joycons_info'][dev['serial']]
70+
else:
71+
joycon = JoyCon(dev['vendor_id'], dev['product_id'], dev['serial'])
72+
# Wait for initial data
73+
for _ in range(3):
74+
time.sleep(0.05)
75+
battery_level = joycon.get_battery_level()
76+
if battery_level > 0:
77+
break
78+
79+
color = '#%02x%02x%02x' % joycon.color_body
80+
joycon.__del__()
81+
82+
info = {
83+
'vendor_id': dev['vendor_id'],
84+
'product_id': dev['product_id'],
85+
'serial': dev['serial'],
86+
'name': dev['product_string'],
87+
'color': color,
88+
'battery_level': battery_level,
89+
'is_left': joycon.is_left(),
90+
'state': PairingState.IDLE.value,
91+
'pairing_code': '',
92+
}
93+
94+
app['joycons_info'][dev['serial']] = info
95+
96+
joycons.append(info)
97+
98+
return sorted(joycons, key=lambda x: (x['name'], x['color'], x['serial']))
99+
100+
101+
async def connect_joycon(app, ws, data):
102+
async def on_joydance_state_changed(serial, state):
103+
print(serial, state)
104+
app['joycons_info'][serial]['state'] = state.value
105+
try:
106+
await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, app['joycons_info'][serial])
107+
except Exception as e:
108+
print(e)
109+
110+
print(data)
111+
112+
serial = data['joycon_serial']
113+
product_id = app['joycons_info'][serial]['product_id']
114+
vendor_id = app['joycons_info'][serial]['vendor_id']
115+
116+
pairing_method = data['pairing_method']
117+
host_ip_addr = data['host_ip_addr']
118+
console_ip_addr = data['console_ip_addr']
119+
pairing_code = data['pairing_code']
120+
121+
if not is_valid_pairing_method(pairing_method):
122+
return
123+
124+
if pairing_method == PairingMethod.DEFAULT.value:
125+
if not is_valid_ip_address(host_ip_addr) or not is_valid_pairing_code(pairing_code):
126+
return
127+
128+
if pairing_method == PairingMethod.FAST.value and not is_valid_ip_address(console_ip_addr):
129+
return
130+
131+
config_parser = parse_config()
132+
config = dict(config_parser.items('joydance'))
133+
config['pairing_code'] = pairing_code
134+
config['pairing_method'] = pairing_method
135+
config['host_ip_addr'] = host_ip_addr
136+
config['console_ip_addr'] = console_ip_addr
137+
config_parser['joydance'] = config
138+
save_config(config_parser)
139+
140+
app['joycons_info'][serial]['pairing_code'] = pairing_code
141+
joycon = ButtonEventJoyCon(vendor_id, product_id, serial)
142+
143+
if pairing_method == PairingMethod.DEFAULT.value:
144+
console_ip_addr = None
145+
146+
joydance = JoyDance(
147+
joycon,
148+
pairing_code=pairing_code,
149+
host_ip_addr=host_ip_addr,
150+
console_ip_addr=console_ip_addr,
151+
on_state_changed=on_joydance_state_changed,
152+
accel_acquisition_freq_hz=config['accel_acquisition_freq_hz'],
153+
accel_acquisition_latency=config['accel_acquisition_latency'],
154+
accel_max_range=config['accel_max_range'],
155+
)
156+
app['joydance_connections'][serial] = joydance
157+
158+
asyncio.create_task(joydance.pair())
159+
160+
161+
async def disconnect_joycon(app, ws, data):
162+
print(data)
163+
serial = data['joycon_serial']
164+
joydance = app['joydance_connections'][serial]
165+
app['joycons_info'][serial]['state'] = PairingState.IDLE
166+
167+
await joydance.disconnect()
168+
try:
169+
await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, {
170+
'joycon_serial': serial,
171+
'state': PairingState.IDLE.value,
172+
})
173+
except Exception:
174+
pass
175+
176+
177+
def parse_config():
178+
parser = ConfigParser()
179+
parser.read('config.cfg')
180+
181+
if 'joydance' not in parser:
182+
parser['joydance'] = DEFAULT_CONFIG
183+
else:
184+
tmp_config = DEFAULT_CONFIG.copy()
185+
for key in tmp_config:
186+
if key in parser['joydance']:
187+
val = parser['joydance'][key]
188+
if key == 'pairing_method':
189+
if not is_valid_pairing_method(val):
190+
val = PairingMethod.DEFAULT.value
191+
elif key == 'host_ip_addr' or key == 'console_ip_addr':
192+
if not(is_valid_ip_address(val)):
193+
val = ''
194+
elif key == 'pairing_code':
195+
if not is_valid_pairing_code(val):
196+
val = ''
197+
elif key.startswith('accel_'):
198+
try:
199+
val = int(val)
200+
except Exception:
201+
val = DEFAULT_CONFIG[key]
202+
203+
tmp_config[key] = val
204+
205+
parser['joydance'] = tmp_config
206+
207+
if not parser['joydance']['host_ip_addr']:
208+
host_ip_addr = get_host_ip()
209+
if host_ip_addr:
210+
parser['joydance']['host_ip_addr'] = host_ip_addr
211+
212+
save_config(parser)
213+
return parser
214+
215+
216+
def is_valid_pairing_code(val):
217+
return re.match(REGEX_PAIRING_CODE, val) is not None
218+
219+
220+
def is_valid_ip_address(val):
221+
return re.match(REGEX_LOCAL_IP_ADDRESS, val) is not None
222+
223+
224+
def is_valid_pairing_method(val):
225+
return val in [PairingMethod.DEFAULT.value, PairingMethod.FAST.value]
226+
227+
228+
def get_host_ip():
229+
try:
230+
for ip in socket.gethostbyname_ex(socket.gethostname())[2]:
231+
if ip.startswith('192.168'):
232+
return ip
233+
except Exception:
234+
pass
235+
236+
return None
237+
238+
239+
def save_config(parser):
240+
with open('config.cfg', 'w') as fp:
241+
parser.write(fp)
242+
243+
244+
async def html_handler(request):
245+
config = dict((parse_config()).items('joydance'))
246+
with open('static/index.html', 'r') as f:
247+
html = f.read()
248+
html = html.replace('[[CONFIG]]', json.dumps(config))
249+
html = html.replace('[[VERSION]]', JOYDANCE_VERSION)
250+
return web.Response(text=html, content_type='text/html')
251+
252+
253+
async def ws_send_response(ws, cmd, data):
254+
resp = {
255+
'cmd': 'resp_' + cmd.value,
256+
'data': data,
257+
}
258+
await ws.send_json(resp)
259+
260+
261+
async def websocket_handler(request):
262+
ws = web.WebSocketResponse()
263+
await ws.prepare(request)
264+
265+
async for msg in ws:
266+
if msg.type == WSMsgType.TEXT:
267+
msg = msg.json()
268+
try:
269+
cmd = WsCommand(msg['cmd'])
270+
except ValueError:
271+
print('Invalid cmd:', msg['cmd'])
272+
continue
273+
274+
if cmd == WsCommand.GET_JOYCON_LIST:
275+
joycon_list = await get_joycon_list(request.app)
276+
await ws_send_response(ws, cmd, joycon_list)
277+
elif cmd == WsCommand.CONNECT_JOYCON:
278+
await connect_joycon(request.app, ws, msg['data'])
279+
await ws_send_response(ws, cmd, {})
280+
elif cmd == WsCommand.DISCONNECT_JOYCON:
281+
await disconnect_joycon(request.app, ws, msg['data'])
282+
await ws_send_response(ws, cmd, {})
283+
elif msg.type == WSMsgType.ERROR:
284+
print('ws connection closed with exception %s' %
285+
ws.exception())
286+
287+
return ws
288+
289+
290+
def favicon_handler(request):
291+
return web.FileResponse('static/favicon.png')
292+
293+
294+
app = web.Application()
295+
app['joydance_connections'] = {}
296+
app['joycons_info'] = {}
297+
298+
app.add_routes([
299+
web.get('/', html_handler),
300+
web.get('/favicon.png', favicon_handler),
301+
web.get('/ws', websocket_handler),
302+
web.static('/css', 'static/css'),
303+
web.static('/js', 'static/js'),
304+
])
305+
306+
web.run_app(app, port=32623)

0 commit comments

Comments
 (0)