Skip to content

Commit 4b9a8eb

Browse files
committed
Initial
0 parents  commit 4b9a8eb

15 files changed

Lines changed: 1281 additions & 0 deletions

File tree

.gitignore

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
# For a library or package, you might want to ignore these files since the code is
87+
# intended to run in multiple environments; otherwise, check them in:
88+
# .python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# poetry
98+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99+
# This is especially recommended for binary packages to ensure reproducibility, and is more
100+
# commonly ignored for libraries.
101+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102+
#poetry.lock
103+
104+
# pdm
105+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106+
#pdm.lock
107+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108+
# in version control.
109+
# https://pdm.fming.dev/#use-with-ide
110+
.pdm.toml
111+
112+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113+
__pypackages__/
114+
115+
# Celery stuff
116+
celerybeat-schedule
117+
celerybeat.pid
118+
119+
# SageMath parsed files
120+
*.sage.py
121+
122+
# Environments
123+
.env
124+
.venv
125+
env/
126+
venv/
127+
ENV/
128+
env.bak/
129+
venv.bak/
130+
131+
# Spyder project settings
132+
.spyderproject
133+
.spyproject
134+
135+
# Rope project settings
136+
.ropeproject
137+
138+
# mkdocs documentation
139+
/site
140+
141+
# mypy
142+
.mypy_cache/
143+
.dmypy.json
144+
dmypy.json
145+
146+
# Pyre type checker
147+
.pyre/
148+
149+
# pytype static type analyzer
150+
.pytype/
151+
152+
# Cython debug symbols
153+
cython_debug/
154+
155+
# PyCharm
156+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158+
# and can be added to the global gitignore or merged into this file. For a more nuclear
159+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160+
#.idea/

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.formatting.provider": "autopep8"
3+
}

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# VRCJoyCon
2+
3+
Link Nintendo Switch Joy-Cons to VRChat! Uses the new [OSC system](https://docs.vrchat.com/docs/osc-overview).
4+
5+
For now, only simple on/off rumble haptics are supported!
6+
7+
# Requirements
8+
9+
- Nintendo Switch Joy-Con controller(s). **Only tested with knockoffs from Aliexpress.**
10+
- A customisable avatar or a compatible avatar
11+
- Unity editor
12+
- `vrcjoycon.exe` from this repository's Releases
13+
14+
# TODO
15+
- Example haptics avatar
16+
- Debug help
17+
- Button input
18+
19+
# Haptics: Setting Up / Usage
20+
21+
**Unity**
22+
1. Position one or multiple [Contact Receivers](https://docs.vrchat.com/docs/contacts#vrccontactreceiver) components to your chosen avatar bone
23+
1. Choose at least some collision tags or you will receive no contacts
24+
2. Haptics can be set to `local only`. `Allow Self` is recommended for testing.
25+
3. Select `Proximity` from `Receiver Type`.
26+
4. Set target parameter to `joyconrumble1`. For right controller choose `joyconrumble2`.
27+
28+
![componentdetails](images/help2.png)
29+
5. Add the above parameters to your [animator parameters](https://docs.vrchat.com/docs/animator-parameters) with default float value of 0.0. This is used by OSC to relay the status to VRCJoyCon.
30+
31+
![animator](images/help1.png)
32+
33+
**VRChat**
34+
1. Put controllers into pairing mode.
35+
2. Pair controllers manually over Bluetooth with Windows
36+
3. Launch vrcjoycon.exe
37+
4. When pairing is successful, the controllers should vibrate a few times
38+
5. In case of trouble, test with other joycon software first
39+
6. Launch **VRChat** if not already launched
40+
1. From the VRChat's **circular menu**, inside **settings**, inside **OSC**, choose **Enable** OSC. Additional help [here](https://docs.vrchat.com/docs/osc-overview#enabling-it).
41+
(*If the haptics do not work, try reset configuration option in the same menu* **ATTN.** The OSC Debug menu does not help you with debugging haptics, only output)
42+
43+
# Credits / components used
44+
- [joycon-python](https://github.com/tocoteron/joycon-python) library
45+
46+
# License
47+
TODO

images/app.ico

15 KB
Binary file not shown.

images/help1.png

10.6 KB
Loading

images/help2.png

57.3 KB
Loading

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
hidapi
2+
pyglm
3+
pythonosc

src/main.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
2+
from pyjoycon import JoyCon, get_R_id, get_L_id, joycon
3+
import logging,sys,os,threading,time
4+
from pythonosc import dispatcher
5+
from pythonosc import osc_server
6+
7+
from os import system
8+
system("title VRChat Joy-Con OSC Connector")
9+
10+
joyconrumble = [False, False]
11+
joycons=[False,False]
12+
lock = threading.Lock()
13+
14+
def headpatter_thread(conGetter, conid):
15+
for i in range(13):
16+
joycon_id = False
17+
18+
while True:
19+
with lock:
20+
joycon_id = conGetter()
21+
if joycon_id and joycon_id[0]:
22+
break
23+
time.sleep(0.4)
24+
25+
id = conid-1
26+
name = conid == 1 and "LEFT" or "RIGHT"
27+
with lock:
28+
joycon = JoyCon(*joycon_id)
29+
joycons[id]=joycon
30+
31+
print("\nFound JoyCon", name,"\n")
32+
with lock:
33+
joycon.set_player_lamp_on(conid) # required to keep controller running
34+
35+
logging.debug("Testing vibration")
36+
time.sleep(1)
37+
with lock:
38+
joycon.rumble_simple()
39+
time.sleep(1.5)
40+
with lock:
41+
joycon.rumble_simple()
42+
time.sleep(0.5)
43+
with lock:
44+
joycon.rumble_stop()
45+
logging.debug("Vibrated")
46+
47+
while joycon.connected():
48+
time.sleep(0.4) # TODO: Signaling, sleep otherwise
49+
pat_status = joyconrumble[id]
50+
if pat_status:
51+
with lock:
52+
joycon.rumble_simple()
53+
print("Vibrating", name)
54+
55+
elif pat_status is not False:
56+
joyconrumble[id] = False
57+
print("STOP", name)
58+
with lock:
59+
joycon.rumble_stop()
60+
print("LOST JOYCON",name)
61+
with lock:
62+
del joycon
63+
joycons[id]=False
64+
print("TOO MANY FAILURES, CLOSING")
65+
66+
threads = []
67+
server: osc_server.ThreadingOSCUDPServer = None
68+
69+
def startOSC():
70+
global server
71+
def joyconrumble_1_handler(address, *args):
72+
logging.debug("joyconrumble_1_handler %s %s", str(address), str(args))
73+
joyconrumble[0] = args[0]
74+
75+
def joyconrumble_2_handler(address, *args):
76+
logging.debug("joyconrumble_2_handler %s %s", str(address), str(args))
77+
joyconrumble[1] = args[0]
78+
79+
d = dispatcher.Dispatcher()
80+
d.map("/avatar/parameters/joyconrumble1", joyconrumble_1_handler)
81+
d.map("/avatar/parameters/joyconrumble2", joyconrumble_2_handler)
82+
server = osc_server.ThreadingOSCUDPServer(
83+
("127.0.0.1", 9001), d)
84+
print("Listening on default OSC port 8991 at {}".format(server.server_address))
85+
print("")
86+
87+
def watchdog():
88+
while True:
89+
anyAlive=False
90+
for t in threads:
91+
t.join(0.2)
92+
if t.is_alive():
93+
anyAlive=True
94+
if not anyAlive:
95+
server.shutdown()
96+
print("\nFATAL: Lost all JoyCon threads\n")
97+
return
98+
"""
99+
100+
hadAny=False
101+
anyAlive=False
102+
for id,c in enumerate(joycons):
103+
if c==False:
104+
continue
105+
hadAny=True
106+
107+
# TODO: Race condition here?
108+
c._update_input_report_thread.join(0.2)
109+
if c._update_input_report_thread.is_alive():
110+
anyAlive=True
111+
112+
if hadAny and not anyAlive:
113+
server.shutdown()
114+
print("\nFATAL: Lost all JoyCons\n")
115+
return
116+
117+
"""
118+
119+
def startJoyCons():
120+
print("Attempting to locate joycons")
121+
for conGetter, id in [(get_L_id, 1), (get_R_id, 2)]:
122+
x = threading.Thread(target=headpatter_thread,
123+
args=(conGetter, id), daemon=True,name="joyconController"+str(id))
124+
x.start()
125+
threads.append(x)
126+
127+
# Watchdog
128+
x = threading.Thread(target=watchdog,
129+
args=(), daemon=True,name="watchdog")
130+
x.start()
131+
132+
if __name__ == "__main__":
133+
startOSC()
134+
startJoyCons()
135+
server.serve_forever()
136+
input("SHUTTING DOWN. PRESS ENTER TO CLOSE.")

src/pyjoycon/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from .joycon import JoyCon
2+
from .wrappers import PythonicJoyCon # as JoyCon
3+
from .gyro import GyroTrackingJoyCon
4+
from .event import ButtonEventJoyCon
5+
from .device import get_device_ids, get_ids_of_type
6+
from .device import is_id_L
7+
from .device import get_R_ids, get_L_ids
8+
from .device import get_R_id, get_L_id
9+
10+
11+
__version__ = "0.2.4"
12+
13+
__all__ = [
14+
"ButtonEventJoyCon",
15+
"GyroTrackingJoyCon",
16+
"JoyCon",
17+
"PythonicJoyCon",
18+
"get_L_id",
19+
"get_L_ids",
20+
"get_R_id",
21+
"get_R_ids",
22+
"get_device_ids",
23+
"get_ids_of_type",
24+
"is_id_L",
25+
]

src/pyjoycon/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
JOYCON_VENDOR_ID = 0x057E
2+
JOYCON_L_PRODUCT_ID = 0x2006
3+
JOYCON_R_PRODUCT_ID = 0x2007
4+
JOYCON_PRODUCT_IDS = (JOYCON_L_PRODUCT_ID, JOYCON_R_PRODUCT_ID)

0 commit comments

Comments
 (0)