Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add systemd unit and misc improvements #6

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
======
================
joycond-cemuhook
======
================

Support for cemuhook's UDP protocol for joycond devices for use with emulators like Dolphin, Cemu, Citra, Yuzu, etc.

Expand All @@ -14,15 +14,26 @@ Supports up to 4 controllers from the following:
- Right Joycon

How to use
--------
- Install `dkms-hid-nintendo <https://github.com/nicman23/dkms-hid-nintendo>`_ (if your kernel doesn't include the hid_nintendo driver)
- Install the `joycond <https://github.com/DanielOgorchock/joycond>`_ userspace driver
- Connect your Nintendo Switch controllers and assign them as intended (using the respective L+R)
- Run joycond-cemuhook: ``python3 joycond-cemuhook.py``
- Open a compatible emulator and enable cemuhook UDP motion input
----------
1. Install `dkms-hid-nintendo <https://github.com/nicman23/dkms-hid-nintendo>`_ (if your kernel doesn't include the hid_nintendo driver)
2. Install the `joycond <https://github.com/DanielOgorchock/joycond>`_ userspace driver
3. Connect your Nintendo Switch controllers and assign them as intended (using the respective L+R)
4. Run joycond-cemuhook: ``./joycond-cemuhook.py`` or ``python3 joycond-cemuhook.py``
5. Open a compatible emulator and enable cemuhook UDP motion input

Systemd unit
------------
Instead of manually running ``joycond-cemuhook.py`` you can use the provided systemd-unit to start it automatically:

::

# cp joycond-cemuhook.py /usr/local/bin/
$ cp joycond-cemuhook.service ~/.config/systemd/user/
$ systemctl --user enable --now joycond-cemuhook.service


Media
--------
-----

The Legend of Zelda: Swkyward Sword on Dolphin

Expand Down
138 changes: 78 additions & 60 deletions joycond-cemuhook.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#! /usr/bin/env python3

import signal
import sys
import evdev
from threading import Thread
Expand All @@ -9,16 +12,19 @@
import dbus
import json


def clamp(my_value, min_value, max_value):
return max(min(my_value, max_value), min_value)


def abs_to_button(value):
if value > 0.75*255:
value = 255
else:
value = 0
return value


class Message(list):
Types = dict(version=bytes([0x00, 0x00, 0x10, 0x00]),
ports=bytes([0x01, 0x00, 0x10, 0x00]),
Expand Down Expand Up @@ -46,6 +52,7 @@ def __init__(self, message_type, data):
crc = crc32(bytes(self)) & 0xffffffff
self[8:12] = bytes(struct.pack('<I', crc))


class SwitchDevice:
def __init__(self, server, device, motion_device):
self.server = server
Expand Down Expand Up @@ -101,7 +108,7 @@ def __init__(self, server, device, motion_device):
self.motion_x = 0
self.motion_y = 0
self.motion_z = 0

self.accel_x = 0
self.accel_y = 0
self.accel_z = 0
Expand All @@ -113,7 +120,7 @@ def __init__(self, server, device, motion_device):
self.motion_event_thread = Thread(target=self.handle_motion_events)
self.motion_event_thread.daemon = True
self.motion_event_thread.start()

def handle_motion_events(self):
if self.motion_device:
try:
Expand Down Expand Up @@ -153,7 +160,7 @@ def handle_motion_events(self):
except(OSError, RuntimeError) as e:
print("Device motion disconnected: " + self.name)
asyncio.get_event_loop().close()

def handle_events(self):
try:
asyncio.set_event_loop(asyncio.new_event_loop())
Expand All @@ -164,7 +171,7 @@ def handle_events(self):
if event.type == evdev.ecodes.EV_ABS:
for ps_key in self.keymap:
key_mine = self.keymap.get(ps_key, None)
if key_mine == None:
if key_mine is None:
continue

if event.code == evdev.ecodes.ecodes.get(key_mine.replace("-", ""), None):
Expand All @@ -185,7 +192,7 @@ def handle_events(self):
self.server.report_clean(self)
self.disconnected = True
asyncio.get_event_loop().close()

def get_battery_level(self):
bus = dbus.SystemBus()
upower = bus.get_object('org.freedesktop.UPower', '/org/freedesktop/UPower')
Expand All @@ -199,15 +206,15 @@ def get_battery_level(self):

if properties["Serial"] == self.serial:
return properties["Percentage"]

return None

def get_report(self):
report = self.state

battery = self.get_battery_level()

if battery == None:
if battery is None:
report["battery"] = 0x00
elif battery < 10:
report["battery"] = 0x01
Expand All @@ -219,14 +226,15 @@ def get_report(self):
report["battery"] = 0x04
else:
report["battery"] = 0x05

self.state["battery"] = report["battery"]

if self.device == None:
if self.device is None:
return report

return report


class UDPServer:
def __init__(self, host='', port=26760):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Expand All @@ -243,7 +251,7 @@ def _res_ports(self, index):
0x00, # state (disconnected)
0x03, # model (generic)
0x01, # connection type (usb)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Mac
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Mac
0x00, # battery (charged)
0x00, # ?
]
Expand All @@ -262,7 +270,7 @@ def _res_ports(self, index):
]

return Message('ports', data)

@staticmethod
def _compat_ord(value):
return ord(value) if sys.version_info < (3, 0) else value
Expand Down Expand Up @@ -298,7 +306,7 @@ def _handle_request(self, request):
self._req_data(message, address)
else:
print('Unknown message type: ' + str(msg_type))

def _res_data(self, message):
now = time.time()
for address, timestamp in self.clients.copy().items():
Expand All @@ -307,39 +315,24 @@ def _res_data(self, message):
else:
print('[udp] Client disconnected: {0[0]}:{0[1]}'.format(address))
del self.clients[address]

def _handle_request(self, request):
message, address = request

# client_id = message[12:16]
msg_type = message[16:20]

if msg_type == Message.Types['version']:
return
elif msg_type == Message.Types['ports']:
self._req_ports(message, address)
elif msg_type == Message.Types['data']:
self._req_data(message, address)
else:
print('[udp] Unknown message type: ' + str(msg_type))

def report(self, device):
if device == None:
if device is None:
return None
if device.device == None:

if device.device is None:
return None

i = self.slots.index(device) if device in self.slots else -1

if i == -1:
return None

device_state = device.get_report()

data = [
i & 0xff, # pad id
0x02 if device.device != None else 0x00, # state (connected)
0x02 if device.device is not None else 0x00, # state (connected)
0x02, # model (generic)
0x02, # connection type (usb)
device.mac[0], device.mac[1], device.mac[2], # MAC1
Expand Down Expand Up @@ -409,7 +402,7 @@ def report(self, device):

data.extend(bytes(struct.pack('<Q', int(time.time() * 10**6))))

if device.motion_device != None:
if device.motion_device is not None:
if device.motion_device.name == "Nintendo Switch Pro Controller IMU":
sensors = [
device.accel_y / 4096,
Expand Down Expand Up @@ -442,9 +435,9 @@ def report(self, device):

for sensor in sensors:
data.extend(struct.pack('<f', float(sensor)))

self._res_data(bytes(Message('data', data)))

def report_clean(self, device):
i = self.slots.index(device) if device in self.slots else -1

Expand All @@ -453,28 +446,29 @@ def report_clean(self, device):
0x00, # state (disconnected)
0x03, # model (generic)
0x01, # connection type (usb)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Mac
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Mac
0x00, # battery (charged)
0x00, # ?
]

self._res_data(bytes(Message('data', data)))

def handle_devices(self):
asyncio.set_event_loop(asyncio.new_event_loop())

print("Looking for Nintendo Switch controllers...")

while True:

error = False
while not error:
try:
evdev_devices = [evdev.InputDevice(path) for path in evdev.list_devices()]

for d in evdev_devices:
if d.name == "Nintendo Switch Left Joy-Con" or \
d.name == "Nintendo Switch Right Joy-Con" or \
d.name == "Nintendo Switch Pro Controller" or \
d.name == "Nintendo Switch Combined Joy-Cons":
found = True if any(my_device.device == d for my_device in self.slots if my_device != None) else False
d.name == "Nintendo Switch Right Joy-Con" or \
d.name == "Nintendo Switch Pro Controller" or \
d.name == "Nintendo Switch Combined Joy-Cons":
found = True if any(my_device.device == d for my_device in self.slots if my_device is not None) else False

if not found:
motion_d = None
Expand All @@ -483,36 +477,39 @@ def handle_devices(self):
if dd.uniq == d.uniq and dd != d:
motion_d = dd
break
if motion_d == None:

if motion_d is None:
print("Select motion provider for "+d.name+": ")
for i, dd in enumerate(evdev_devices):
print(str(i) + " " + dd.name + " " + dd.uniq)
motion_d = evdev_devices[int(input(""))]

for i in range(4):
if self.slots[i] == None:
self.slots[i] = SwitchDevice(self, d, motion_d)
print("Found "+d.name+" - "+d.uniq)
if self.slots[i] is None:
try:
self.slots[i] = SwitchDevice(self, d, motion_d)
print("Found "+d.name+" - "+d.uniq)
except Exception as e:
error = True
break

self.print_slots()

for i in range(4):
if self.slots[i] != None and self.slots[i].disconnected == True:
if self.slots[i] is not None and self.slots[i].disconnected is True:
self.slots[i] = None
self.print_slots()

time.sleep(0.2) # sleep for 0.2 seconds

time.sleep(0.2) # sleep for 0.2 seconds

except:
pass



def print_slots(self):
slots_print = []

for i in range(4):
if self.slots[i] == None:
if self.slots[i] is None:
slots_print.append("0")
else:
if "Left" in self.slots[i].name:
Expand Down Expand Up @@ -542,6 +539,27 @@ def start(self):

self.device_thread.join()

def stop(self, sig=None, frame=None, err=None):
if sig is not None:
print("Stopping server")
self.report_clean(self)
self.disconnected = True
asyncio.get_event_loop().close()
print(err)
if err:
raise Exception(err)




server = UDPServer('127.0.0.1', 26760)
server.start()

# Handle CTRL+C and systemctl stop default signal
signal.signal(signal.SIGINT, server.stop)
signal.signal(signal.SIGTERM, server.stop)

try:
server.start()
except Exception as e:
print("hey")
sys.exit(e)
10 changes: 10 additions & 0 deletions joycond-cemuhook.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Cemuhook's UDP protocol for joycond devices
After=joycond.service

[Service]
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/local/bin/joycond-cemuhook.py

[Install]
WantedBy=multi-user.target