diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 69281e7..0000000 --- a/.gitignore +++ /dev/null @@ -1,134 +0,0 @@ -.idea - -# PYTHON - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - diff --git a/README.md b/README.md index 9d486f1..9d0a6e0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Tested with Raspberry Pi 4B Raspbian GNU/Linux 10 (buster) - Start the program ```bash -sudo python3 run_and_pair_switch.py +sudo python3 run_test_controller_buttons.py ``` - Open the "Change Grip/Order" menu of the Switch - The emulated controller pairs with the Switch and automatically navigates to the "Test Controller Buttons" menu diff --git a/joycontrol/__init__.py b/joycontrol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/button_state.py b/joycontrol/button_state.py similarity index 99% rename from button_state.py rename to joycontrol/button_state.py index 37b14fd..0bc51b5 100644 --- a/button_state.py +++ b/joycontrol/button_state.py @@ -1,4 +1,4 @@ -import utils +from joycontrol import utils class ButtonState: diff --git a/controller.py b/joycontrol/controller.py similarity index 100% rename from controller.py rename to joycontrol/controller.py diff --git a/controller_state.py b/joycontrol/controller_state.py similarity index 91% rename from controller_state.py rename to joycontrol/controller_state.py index fc759b7..2e0ba69 100644 --- a/controller_state.py +++ b/joycontrol/controller_state.py @@ -1,7 +1,7 @@ import asyncio -from button_state import ButtonState -from protocol import ControllerProtocol +from joycontrol.button_state import ButtonState +from joycontrol.protocol import ControllerProtocol class ControllerState: diff --git a/device.py b/joycontrol/device.py similarity index 98% rename from device.py rename to joycontrol/device.py index 6eace31..1a42281 100644 --- a/device.py +++ b/joycontrol/device.py @@ -3,7 +3,7 @@ import uuid import dbus -import utils +from joycontrol import utils logger = logging.getLogger(__name__) diff --git a/logging_default.py b/joycontrol/logging_default.py similarity index 100% rename from logging_default.py rename to joycontrol/logging_default.py diff --git a/profile/sdp_record_hid_pro.xml b/joycontrol/profile/sdp_record_hid.xml similarity index 100% rename from profile/sdp_record_hid_pro.xml rename to joycontrol/profile/sdp_record_hid.xml diff --git a/protocol.py b/joycontrol/protocol.py similarity index 97% rename from protocol.py rename to joycontrol/protocol.py index 06748e0..5cba0c4 100644 --- a/protocol.py +++ b/joycontrol/protocol.py @@ -3,8 +3,8 @@ import logging from asyncio import BaseTransport, BaseProtocol from typing import Optional, Union, Tuple, Text -from controller import Controller -from report import OutputReport, SubCommand, InputReport +from joycontrol.controller import Controller +from joycontrol.report import OutputReport, SubCommand, InputReport logger = logging.getLogger(__name__) diff --git a/report.py b/joycontrol/report.py similarity index 97% rename from report.py rename to joycontrol/report.py index 21fd3a4..981c8f9 100644 --- a/report.py +++ b/joycontrol/report.py @@ -1,8 +1,8 @@ import asyncio from enum import Enum -from button_state import ButtonState -from controller import Controller +from joycontrol.button_state import ButtonState +from joycontrol.controller import Controller class InputReport: diff --git a/joycontrol/server.py b/joycontrol/server.py new file mode 100644 index 0000000..252e747 --- /dev/null +++ b/joycontrol/server.py @@ -0,0 +1,72 @@ +import asyncio +import logging +import os +import socket + +import joycontrol +from joycontrol import utils +from joycontrol.device import HidDevice +from joycontrol.report import InputReport +from joycontrol.transport import L2CAP_Transport + +PROFILE_PATH = os.path.join(os.path.dirname(joycontrol.__file__), 'profile/sdp_record_hid.xml') +logger = logging.getLogger(__name__) + + +async def _send_empty_input_reports(transport): + report = InputReport() + + while True: + await transport.write(report) + await asyncio.sleep(1) + + +async def create_hid_server(protocol_factory, ctl_psm, itr_psm): + ctl_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) + itr_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) + + # for some reason we need to restart bluetooth here, the Switch does not connect to the sockets if we don't... + logger.info('Restarting bluetooth service...') + await utils.run_system_command('systemctl restart bluetooth.service') + await asyncio.sleep(1) + + ctl_sock.setblocking(False) + itr_sock.setblocking(False) + + ctl_sock.bind((socket.BDADDR_ANY, ctl_psm)) + itr_sock.bind((socket.BDADDR_ANY, itr_psm)) + + ctl_sock.listen(1) + itr_sock.listen(1) + + protocol = protocol_factory() + + hid = HidDevice() + # setting bluetooth adapter name and class to the device we wish to emulate + await hid.set_name(protocol.controller.device_name()) + await hid.set_class() + + logger.info('Advertising the Bluetooth SDP record...') + hid.register_sdp_record(PROFILE_PATH) + hid.discoverable() + + loop = asyncio.get_event_loop() + client_ctl, ctl_address = await loop.sock_accept(ctl_sock) + logger.info(f'Accepted connection at psm {ctl_psm} from {ctl_address}') + client_itr, itr_address = await loop.sock_accept(itr_sock) + logger.info(f'Accepted connection at psm {itr_psm} from {itr_address}') + assert ctl_address[0] == itr_address[0] + + transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, 50) + protocol.connection_made(transport) + + # send some empty input reports until the switch decides to reply + future = asyncio.ensure_future(_send_empty_input_reports(transport)) + await protocol.wait_for_output_report() + future.cancel() + try: + await future + except asyncio.CancelledError: + pass + + return transport, protocol diff --git a/transport.py b/joycontrol/transport.py similarity index 98% rename from transport.py rename to joycontrol/transport.py index 18b8176..506b7e3 100644 --- a/transport.py +++ b/joycontrol/transport.py @@ -2,7 +2,7 @@ import asyncio import logging from typing import Any -from report import InputReport +from joycontrol.report import InputReport logger = logging.getLogger(__name__) diff --git a/utils.py b/joycontrol/utils.py similarity index 100% rename from utils.py rename to joycontrol/utils.py diff --git a/run_and_pair_switch.py b/run_test_controller_buttons.py similarity index 54% rename from run_and_pair_switch.py rename to run_test_controller_buttons.py index a7b933b..80cff91 100644 --- a/run_and_pair_switch.py +++ b/run_test_controller_buttons.py @@ -1,69 +1,15 @@ import asyncio import logging import os -import socket -import logging_default as log -import utils -from controller_state import ButtonState, ControllerState -from device import HidDevice -from protocol import controller_protocol_factory, Controller -from report import InputReport -from transport import L2CAP_Transport +from joycontrol import logging_default as log +from joycontrol.controller_state import ButtonState, ControllerState +from joycontrol.protocol import controller_protocol_factory, Controller +from joycontrol.server import create_hid_server logger = logging.getLogger(__name__) -async def create_hid_server(protocol_factory, ctl_psm, itr_psm): - ctl_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) - itr_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) - - # for some reason we need to restart bluetooth here, the Switch does not connect to the sockets if we don't... - logger.info('Restarting bluetooth service...') - await utils.run_system_command('systemctl restart bluetooth.service') - await asyncio.sleep(1) - - ctl_sock.setblocking(False) - itr_sock.setblocking(False) - - ctl_sock.bind((socket.BDADDR_ANY, ctl_psm)) - itr_sock.bind((socket.BDADDR_ANY, itr_psm)) - - ctl_sock.listen(1) - itr_sock.listen(1) - - protocol = protocol_factory() - - hid = HidDevice() - # setting bluetooth adapter name and class to the device we wish to emulate - await hid.set_name(protocol.controller.device_name()) - await hid.set_class() - - logger.info('Advertising the Bluetooth SDP record...') - hid.register_sdp_record('profile/sdp_record_hid_pro.xml') - hid.discoverable() - - loop = asyncio.get_event_loop() - client_ctl, ctl_address = await loop.sock_accept(ctl_sock) - logger.info(f'Accepted connection at psm {ctl_psm} from {ctl_address}') - client_itr, itr_address = await loop.sock_accept(itr_sock) - logger.info(f'Accepted connection at psm {itr_psm} from {itr_address}') - assert ctl_address[0] == itr_address[0] - - transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, 50) - protocol.connection_made(transport) - - return transport, protocol - - -async def send_empty_input_reports(transport): - report = InputReport() - - while True: - await transport.write(report) - await asyncio.sleep(1) - - async def button_push(controller_state, button, sec=0.1): button_state = ButtonState() @@ -142,15 +88,6 @@ async def test_controller_buttons(controller_state: ControllerState): async def main(): transport, protocol = await create_hid_server(controller_protocol_factory(Controller.PRO_CONTROLLER), 17, 19) - # send some empty input reports until the switch decides to reply - future = asyncio.ensure_future(send_empty_input_reports(transport)) - await protocol.wait_for_output_report() - future.cancel() - try: - await future - except asyncio.CancelledError: - pass - await test_controller_buttons(ControllerState(transport, protocol)) logger.info('Stopping communication...') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fa71630 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ + +from setuptools import setup, find_packages + +setup(name='joycontrol', + version='0.1', + author='Robert Martin', + author_email='martinro@informatik.hu-berlin.de', + description='Emulate Nintendo Switch Controllers over Bluetooth', + packages=find_packages(), + zip_safe=False, + install_requires=[ + # TODO + ] + )