Skip to content

Commit

Permalink
Merge pull request #12 from Victrid/v0.2.0
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
Victrid committed Oct 2, 2022
2 parents e3ed8bc + f598ae1 commit f38b7ac
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 58 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
# libnxctrl
Python Library Emulating Nintendo Switch Controllers

## Installation
There are multiple backends: nxbt and splatplost USB. The nxbt backend can only be used on Linux, while the splatplost USB backend can be used on Linux and Windows.

```bash
pip install libnxctrl[nxbt]
pip install libnxctrl[usb]
```
40 changes: 25 additions & 15 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='libnxctrl',
version='0.1.7',
version='0.2.0',
url='https://github.com/Victrid/libnxctrl',
license='GPLv3',
author='Weihao Jiang',
Expand All @@ -21,22 +21,32 @@
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Natural Language :: English",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
],
install_requires=[
# NXBT requirements
"dbus-python~=1.2.16",
"Flask>=1.1.2,<2.3.0",
"Flask-SocketIO>=5.0.1,<5.4.0",
"eventlet>=0.31,<0.34",
"blessed>=1.17.10,<1.20.0",
"pynput~=1.7.1",
"psutil>=5.6.6,<5.10.0",
"cryptography>=3.3.2,<37.1.0",
# JoyControl requirements
'hid~=1.0.5',
'aioconsole~=0.5.1'
],
python_requires='>=3.9',
install_requires=[],
extras_require={
"nxbt": [
"dbus-python~=1.2.16",
"Flask>=1.1.2,<2.3.0",
"Flask-SocketIO>=5.0.1,<5.4.0",
"eventlet>=0.31,<0.34",
"blessed>=1.17.10,<1.20.0",
"pynput~=1.7.1",
"psutil~=5.6.6",
"cryptography>=3.3.2,<37.1.0",
],
"joycontrol": [
'hid~=1.0.5',
'aioconsole~=0.5.1'
],
"usb": [
"pyserial~=3.5",
]
},
packages=[
"libnxctrl",
"libnxctrl.nxbt.nxbt",
Expand Down
47 changes: 47 additions & 0 deletions src/libnxctrl/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Type

from .wrapper import NXWrapper


def get_available_backend() -> list[str]:
available_backend = []
try:
# NXBT
import dbus
import flask
import flask_socketio
import eventlet
import blessed
import pynput
import psutil
import cryptography
available_backend.append("nxbt")
except ImportError:
pass
try:
# Splatplost USB
import serial
available_backend.append("Splatplost USB")
except ImportError:
pass

return available_backend


def get_backend(backend_name: str) -> Type[NXWrapper]:
if backend_name not in get_available_backend():
raise ValueError(f"Backend {backend_name} is not available.")
if backend_name == "nxbt":
from .nxbt_wrapper import NXBTControl
return NXBTControl
elif backend_name == "mart1no":
raise NotImplementedError("Joycontrol support is not implemented.")
# return Mart1noJoyControl
elif backend_name == "poohl":
raise NotImplementedError("Joycontrol support is not implemented.")
# return PoohlJoyControl
elif backend_name == "Splatplost USB":
from .splatplost_USB import SplatplostUSBControl
return SplatplostUSBControl
else:
raise ValueError(f"Backend {backend_name} is not available.")
19 changes: 0 additions & 19 deletions src/libnxctrl/bluetooth.py

This file was deleted.

153 changes: 153 additions & 0 deletions src/libnxctrl/splatplost_USB.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import ctypes
import re
from enum import IntEnum, IntFlag
from time import sleep
from typing import Optional

import serial

from .wrapper import Button, NXWrapper


class USBInput(ctypes.Structure):
_fields_ = [("index", ctypes.c_uint32), ("ctrl", ctypes.c_uint8), ("buttons", ctypes.c_uint16),
("dpad", ctypes.c_uint8), ("lx", ctypes.c_uint8), ("ly", ctypes.c_uint8), ("rx", ctypes.c_uint8),
("ry", ctypes.c_uint8), ("pressTick", ctypes.c_uint32), ]


class SplatplostUSBControl(NXWrapper):
support_combo = True

class SPUButton(IntFlag):
"""
This value is fixed, do not change it.
"""
NONE = 0x00,
SWITCH_Y = 0x01,
SWITCH_B = 0x02,
SWITCH_A = 0x04,
SWITCH_X = 0x08,
SWITCH_L = 0x10,
SWITCH_R = 0x20,
SWITCH_ZL = 0x40,
SWITCH_ZR = 0x80,
SWITCH_MINUS = 0x100,
SWITCH_PLUS = 0x200,
SWITCH_LCLICK = 0x400,
SWITCH_RCLICK = 0x800,
SWITCH_HOME = 0x1000,
SWITCH_CAPTURE = 0x2000,

class SPU_DPAD(IntEnum):
"""
This value is fixed, do not change it.
"""
CENTER = 0x08,
UP = 0x00,
RIGHT = 0x02,
DOWN = 0x04,
LEFT = 0x06,
RIGHT_UP = 0x01,
RIGHT_DOWN = 0x03,
LEFT_UP = 0x07,
LEFT_DOWN = 0x05,

STICK_MIN = 0
STICK_CENTER = 128
STICK_MAX = 255

def __init__(self, serial_port: str, press_duration_ms: int = 30):
super().__init__(press_duration_ms)

self.serial_port = serial_port
self.serial: Optional[serial.Serial] = None
self.poll_rate = 10
self.report_idx = 0

self.wait_time = 20

def send_report(self, report: USBInput):
report.index = self.report_idx
self.serial.write(report)
acknowledge_info = self.serial.readline().decode("ASCII").strip()
ack_format = r"###ACK ([0-9]+)###"
match = re.match(ack_format, acknowledge_info)
if match:
if self.report_idx != int(match.group(1)):
raise Exception("Report index mismatch")
else:
raise Exception("Invalid acknowledge info")
self.report_idx += 1

def connect(self):
# TODO: Implement connection check
self.serial = serial.Serial(self.serial_port, 115200)
for _ in range(50):
self.button_press(Button.SHOULDER_L | Button.SHOULDER_R)

def button_name_to_SPUButton(self, button_name: Button) -> SPUButton:
button_map = {
Button.A: self.SPUButton.SWITCH_A,
Button.B: self.SPUButton.SWITCH_B,
Button.X: self.SPUButton.SWITCH_X,
Button.Y: self.SPUButton.SWITCH_Y,
Button.SHOULDER_L: self.SPUButton.SWITCH_L,
Button.SHOULDER_R: self.SPUButton.SWITCH_R,
Button.SHOULDER_ZL: self.SPUButton.SWITCH_ZL,
Button.SHOULDER_ZR: self.SPUButton.SWITCH_ZR,
Button.L_STICK_PRESS: self.SPUButton.SWITCH_LCLICK,
Button.R_STICK_PRESS: self.SPUButton.SWITCH_RCLICK,
Button.HOME: self.SPUButton.SWITCH_HOME,
Button.CAPTURE: self.SPUButton.SWITCH_CAPTURE,
Button.MINUS: self.SPUButton.SWITCH_MINUS,
Button.PLUS: self.SPUButton.SWITCH_PLUS,
}

new_button = self.SPUButton.NONE
for button, internal_button in button_map.items():
if button_name & button:
new_button = internal_button | new_button

return new_button

def button_name_to_DPAD(self, button_name: Button) -> SPU_DPAD:
dpad_map = {
Button.DPAD_UP: self.SPU_DPAD.UP,
Button.DPAD_DOWN: self.SPU_DPAD.DOWN,
Button.DPAD_LEFT: self.SPU_DPAD.LEFT,
Button.DPAD_RIGHT: self.SPU_DPAD.RIGHT,
}

new_button = self.SPU_DPAD.CENTER
for button, dpad_button in dpad_map.items():
# Only allow one dpad button to be pressed at a time
if button_name & button:
new_button = dpad_button

return new_button

def button_hold(self, button_name: Button, duration_ms: int):
report = USBInput(buttons=self.button_name_to_SPUButton(button_name).value,
dpad=self.button_name_to_DPAD(button_name).value,
lx=self.STICK_CENTER,
ly=self.STICK_CENTER,
rx=self.STICK_CENTER,
ry=self.STICK_CENTER,
pressTick=1 if duration_ms // self.poll_rate == 0 else duration_ms // self.poll_rate,
)
self.send_report(report)
report = USBInput(buttons=0,
dpad=self.SPU_DPAD.CENTER.value,
lx=self.STICK_CENTER,
ly=self.STICK_CENTER,
rx=self.STICK_CENTER,
ry=self.STICK_CENTER,
pressTick=1 if duration_ms // self.poll_rate == 0 else duration_ms // self.poll_rate,
)
self.send_report(report)

def button_press(self, button_name: Button):
self.button_hold(button_name, self.press_duration_ms)

def disconnect(self):
self.serial.close()
53 changes: 29 additions & 24 deletions src/libnxctrl/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
from abc import abstractmethod
from enum import Enum
from enum import Flag, auto
from typing import Union


class Button(Enum):
A = 0
B = 1
X = 2
Y = 3
DPAD_UP = 4
DPAD_DOWN = 5
DPAD_LEFT = 6
DPAD_RIGHT = 7
L_STICK_PRESS = 8
R_STICK_PRESS = 9
SHOULDER_L = 10
SHOULDER_R = 11
SHOULDER_ZL = 12
SHOULDER_ZR = 13
HOME = 14
CAPTURE = 15
MINUS = 16
PLUS = 17
JCL_SR = 18
JCL_SL = 19
JCR_SR = 20
JCR_SL = 21
class Button(Flag):
A = auto()
B = auto()
X = auto()
Y = auto()
DPAD_UP = auto()
DPAD_DOWN = auto()
DPAD_LEFT = auto()
DPAD_RIGHT = auto()
L_STICK_PRESS = auto()
R_STICK_PRESS = auto()
SHOULDER_L = auto()
SHOULDER_R = auto()
SHOULDER_ZL = auto()
SHOULDER_ZR = auto()
HOME = auto()
CAPTURE = auto()
MINUS = auto()
PLUS = auto()
JCL_SR = auto()
JCL_SL = auto()
JCR_SR = auto()
JCR_SL = auto()


class NXWrapper:
support_combo = False

def combo_supported(self):
return self.support_combo

def __init__(self, press_duration_ms: int = 50, delay_ms: int = 120):
self.press_duration_ms = press_duration_ms
self.delay_ms = delay_ms
Expand Down

0 comments on commit f38b7ac

Please sign in to comment.