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

V12 compatability and Amiibo-NFC support. #110

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3abcdb0
fixed packet-flooding
Oct 30, 2020
6f4c41d
Amiibo read support
Nov 5, 2020
abdc145
working properly w reconnect
Poohl Dec 9, 2020
53a99a5
NFC Amiibo cleanup
Poohl Dec 9, 2020
8df9d0c
typos
Poohl Dec 9, 2020
5d7bfe6
rename MCU and additional flooding mitigation
Poohl Jan 31, 2021
c150d2a
initial writeup
Poohl Feb 8, 2021
4d45f89
no bins
Poohl Feb 8, 2021
18eba52
adapted NFCTag everywhere
Poohl Feb 8, 2021
34889d0
Writing until all data is transfered until EOF
Poohl Feb 20, 2021
f0d0399
Merge master into amiibo_writing
Poohl Feb 27, 2021
126be1d
Merge remote-tracking branch 'upstream/master' into amiibo_edits
Poohl Mar 15, 2021
234de28
fix bluetooth scripts
Poohl Mar 15, 2021
f06f447
dirty hacks doing gods work
Poohl Mar 20, 2021
7d20452
cleand up hacky write support
Poohl Mar 21, 2021
ee63a00
another proxy script
Poohl Mar 21, 2021
ff5dff6
fix bluetooth scripts
Poohl Mar 15, 2021
7704317
another proxy script
Poohl Mar 21, 2021
d488352
cleaned up amiibo writing
Poohl Mar 21, 2021
415e5ee
removed NFC cache, remove now works but switch doesn't care
Poohl Mar 22, 2021
2bf640e
full writing support with hacked remove
Poohl Mar 22, 2021
c2d10ac
Fixed screwup when registering multimple amiibo
Poohl Apr 2, 2021
075b895
proper write lock implementation
Poohl Apr 15, 2021
9d74b18
long amiibos
Poohl Apr 15, 2021
f5f93d9
Make Pairing work again on V12
Poohl Apr 17, 2021
4ccc3b2
documentation on V12 workaround
Poohl Apr 17, 2021
8d14a44
automated unpairing
Poohl Apr 18, 2021
7470666
Live wireshark capture on remote host
Poohl Apr 18, 2021
3b98110
capturing pairing
Poohl Apr 24, 2021
9712bc7
HCI message reader
Poohl Apr 24, 2021
cbb7700
optional flow control
Poohl Apr 24, 2021
cd58968
Updated and improved scripts
Poohl May 1, 2021
fca0c68
flow control and refactoring
Poohl May 1, 2021
1899967
joycon_ip_proxy doc
Poohl May 3, 2021
f8162d7
joyconproxy: @Yamakaky's buffering and conversion to UDP
Poohl May 4, 2021
a4fbdbf
The Grand V12 Fix
Poohl May 7, 2021
35ab59e
Full automation, transport level fix
Poohl May 8, 2021
af940b8
disabled input checking after pairing
Poohl May 8, 2021
f0ef7b4
V12 touchup and finalisation
Poohl May 11, 2021
0b79bf7
housekeeping
Poohl May 11, 2021
642b98a
Merge branch amiibo_writing and V12_fixes into amiibo_edits
Poohl May 11, 2021
548b258
updated Doc
Poohl May 12, 2021
d23fe8e
Fixed my semaphore, added Flow-control by default
Poohl May 12, 2021
666c523
removed MAC change code, updated SDP warning
Poohl May 12, 2021
20b5b62
better doc, automated leave pairing speedup, class check
Poohl May 16, 2021
1d1ccef
Some small fixes to README
kindfulkirby Aug 20, 2021
3e80cb3
Merge pull request #9 from kindfulkirby/patch-1
Poohl Aug 20, 2021
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,8 @@ dmypy.json
# Pyre type checker
.pyre/

# Ubuntu
.Trash-*/

# binarys or dumps
*.bin
120 changes: 91 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,120 @@
# joycontrol

Branch: master->amiibo_edits

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to leave this in the readme for the PR?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's just there because ppl got confused with what Readme is up to date for what branch.
Just tell me and I'll remove it.


Emulate Nintendo Switch Controllers over Bluetooth.

Tested on Ubuntu 19.10, and with Raspberry Pi 3B+ and 4B Raspbian GNU/Linux 10 (buster)
Tested on Raspberry 4B Raspbian, should work on 3B+ too and anything that can do the setup.

## Features
Emulation of JOYCON_R, JOYCON_L and PRO_CONTROLLER. Able to send:
- button commands
- stick state
- ~~nfc data~~ (removed, see [#80](https://github.com/mart1nro/joycontrol/issues/80))
- nfc for amiibo read & owner registration

## Installation
- Install dependencies

Ubuntu: Install the `dbus-python` and `libhidapi-hidraw0` packages
- Install dependencies
Raspbian:
```bash
sudo apt install python3-dbus libhidapi-hidraw0
sudo apt install python3-dbus libhidapi-hidraw0 libbluetooth-dev bluez
```

Arch Linux Derivatives: Install the `hidapi` and `bluez-utils-compat`(AUR) packages


- Clone the repository and install the joycontrol package to get missing dependencies (Note: Controller script needs super user rights, so python packages must be installed as root). In the joycontrol folder run:
Python: (a setup.py is present but not yet up to date)
Note that pip here _has_ to be run as root, as otherwise the packages are not available to the root user.
```bash
sudo pip3 install .
sudo pip3 install aioconsole hid crc8
```
- Consider to disable the bluez "input" plugin, see [#8](https://github.com/mart1nro/joycontrol/issues/8)
If you are unsure if the packages are properly installed, try running `sudo python3` and import each using `import package_name`.

- setup bluetooth
- [I shouldn't have to say this, but] make sure you have a working Bluetooth adapter\
If you are running inside a VM, the PC might but not the VM. Check for a controller using `bluetoothctl show` or `bluetoothctl list`. Also a good indicator it the actual os reporting to not have bluetooth anymore.
- disable SDP [only necessary when pairing]\
change the `ExecStart` parameter in `/lib/systemd/system/bluetooth.service` to `ExecStart=/usr/lib/bluetooth/bluetoothd -C -P sap,input,avrcp`.\
This is to remove the additional reported features as the switch only looks for a controller.\
This also breaks all other Bluetooth gadgets, as this also disabled the needed drivers.
- disable input plugin [experimental alternative to above when not pairing]\
When not pairing, you can get away with only disabling the `input` plugin, only breaking bluetooth-input devices on your PC. Do so by changing `ExecStart` to `ExecStart=/usr/lib/bluetooth/bluetoothd -C -P input` instead.
- Restart bluetooth-deamon to apply the changes:
```bash
sudo systemctl daemon-reload
sudo systemctl restart bluetooth.service
```
- see [Issue #4](https://github.com/Poohl/joycontrol/issues/4) if despite that the switch disconnects randomly.

## Command line interface example
- Run the script
There is a simple CLI (`suco python3 run_controller_cli.py`) provided with this app. Startup-options are:
```
usage: run_controller_cli.py [-h] [-l LOG] [-d DEVICE_ID]
[--spi_flash SPI_FLASH] [-r RECONNECT_BT_ADDR]
[--nfc NFC]
controller

positional arguments:
controller JOYCON_R, JOYCON_L or PRO_CONTROLLER

optional arguments:
-h, --help show this help message and exit
-l LOG, --log LOG BT-communication logfile output
-d DEVICE_ID, --device_id DEVICE_ID
not fully working yet, the BT-adapter to use
--spi_flash SPI_FLASH
controller SPI-memory dump to use
-r RECONNECT_BT_ADDR, --reconnect_bt_addr RECONNECT_BT_ADDR
The Switch console Bluetooth address (or "auto" for
automatic detection), for reconnecting as an already
paired controller.
--nfc NFC amiibo dump placed on the controller. Equivalent to
the nfc command.

```

To use the script:
- start it (this is a minimal example)
```bash
sudo python3 run_controller_cli.py PRO_CONTROLLER
```
This will create a PRO_CONTROLLER instance waiting for the Switch to connect.
- The cli does sanity checks on startup, you might get promps telling you they failed. Check the command-line options and your setup in this case. (Note: not the logging messages). You can however still try to proceed, sometimes it works despite the warnings.

- Afterwards a PRO_CONTROLLER instance waiting for the Switch to connect is created.

- Open the "Change Grip/Order" menu of the Switch
- If you didn't pass the `-r` option, Open the "Change Grip/Order" menu of the Switch and wait for it to pair.

The Switch only pairs with new controllers in the "Change Grip/Order" menu.
- If you already connected the emulated controller once, you can use the reconnect option of the script (`-r <Switch Bluetooth Mac address>`). Don't open the "Change Grip/Order" menu in this case, just make sure the switch is turned on. You can find out a paired mac address using the `bluetoothctl paired-devices` system command or pass `-r auto` as address for automatic detection.

Note: If you already connected an emulated controller once, you can use the reconnect option of the script (-r "\<Switch Bluetooth Mac address>").
This does not require the "Change Grip/Order" menu to be opened. You can find out a paired mac address using the "bluetoothctl" system command.
- After connecting, a command line interface is opened.
Note: Press \<enter> if you don't see a prompt.

- After connecting, a command line interface is opened. Note: Press \<enter> if you don't see a prompt.
Call "help" to see a list of available commands.

Call "help" to see a list of available commands.
## API

- If you call "test_buttons", the emulated controller automatically navigates to the "Test Controller Buttons" menu.
See the `run_controller_cli.py` for an example how to use the API. A minimal example:

```python
from joycontrol.protocol import controller_protocol_factory
from joycontrol.server import create_hid_server
from joycontrol.controller import Controller

# the type of controller to create
controller = Controller.PRO_CONTROLLER # or JOYCON_L or JOYCON_R
# a callback to create the corresponding protocol once a connection is established
factory = controller_protocol_factory(controller)
# start the emulated controller
transport, protocol = await create_hid_server(factory)
# get a reference to the state beeing emulated.
controller_state = protocol.get_controller_state()
# wait for input to be accepted
await controller_state.connect()
# some sample input
controller_state.button_state.set_button('a', True)
# wait for it to be sent at least once
await controller_state.send()
```

## Issues
- Some bluetooth adapters seem to cause disconnects for reasons unknown, try to use an usb adapter instead
- Incompatibility with Bluetooth "input" plugin requires a bluetooth restart, see [#8](https://github.com/mart1nro/joycontrol/issues/8)
- It seems like the Switch is slower processing incoming messages while in the "Change Grip/Order" menu.
This causes flooding of packets and makes pairing somewhat inconsistent.
Not sure yet what exactly a real controller does to prevent that.
A workaround is to use the reconnect option after a controller was paired once, so that
opening of the "Change Grip/Order" menu is not required.
- Some bluetooth adapters seem to cause disconnects for reasons unknown, try to use an usb adapter or a raspi instead.
- Incompatibility with Bluetooth "input" plugin requires it to be disabled (along with the others), see [Issue #8](https://github.com/mart1nro/joycontrol/issues/8)
- The reconnect doesn't ever connect, `bluetoothctl` shows the connection constantly turning on and off. This means the switch tries initial pairing, you have to unpair the switch and try without the `-r` option again.
- ...

## Thanks
Expand All @@ -66,3 +126,5 @@ Call "help" to see a list of available commands.
[Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering)

[console_pairing_session](https://github.com/timmeh87/switchnotes/blob/master/console_pairing_session)

[Hardware Issues thread](https://github.com/Poohl/joycontrol/issues/4)
7 changes: 6 additions & 1 deletion joycontrol/controller_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def connect(self):
"""
Waits until the switch is paired with the controller and accepts button commands
"""
await self._protocol.sig_set_player_lights.wait()
await self._protocol.sig_input_ready.wait()


class ButtonState:
Expand Down Expand Up @@ -160,11 +160,13 @@ def getter():
self.zl, self.zl_is_set = button_method_factory('_byte_3', 7)

def set_button(self, button, pushed=True):
button = button.lower()
if button not in self._available_buttons:
raise ValueError(f'Given button "{button}" is not available to {self.controller.device_name()}.')
getattr(self, button)(pushed=pushed)

def get_button(self, button):
button = button.lower()
if button not in self._available_buttons:
raise ValueError(f'Given button "{button}" is not available to {self.controller.device_name()}.')
return getattr(self, f'{button}_is_set')()
Expand All @@ -186,6 +188,9 @@ def __iter__(self):
def clear(self):
self._byte_1 = self._byte_2 = self._byte_3 = 0

def __bytes__(self):
return bytes([self._byte_1, self._byte_2, self._byte_3])


async def button_press(controller_state, *buttons):
"""
Expand Down
13 changes: 13 additions & 0 deletions joycontrol/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

delay_override = False
delay = 1/15

async def debug(*args):
global delay_override
global delay
if len(args) > 0:
delay_override = True
delay = 1/float(args[0])

def get_delay(old):
return delay if delay_override else old
78 changes: 64 additions & 14 deletions joycontrol/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,73 @@

class HidDevice:
def __init__(self, device_id=None):
self._device_id = device_id
bus = dbus.SystemBus()

# Get Bluetooth adapter from dbus interface
manager = dbus.Interface(bus.get_object('org.bluez', '/'), 'org.freedesktop.DBus.ObjectManager')
for path, ifaces in manager.GetManagedObjects().items():
for path, ifaces in bus.get_object('org.bluez', '/').GetManagedObjects(dbus_interface='org.freedesktop.DBus.ObjectManager').items():
adapter_info = ifaces.get('org.bluez.Adapter1')
if adapter_info is None:
continue
elif device_id is None or device_id == adapter_info['Address'] or path.endswith(str(device_id)):
obj = bus.get_object('org.bluez', path)
self.adapter = dbus.Interface(obj, 'org.bluez.Adapter1')
self.address = adapter_info['Address']
self._adapter_name = path.split('/')[-1]

self.properties = dbus.Interface(self.adapter, 'org.freedesktop.DBus.Properties')
if adapter_info and (device_id is None or device_id == adapter_info['Address'] or path.endswith(str(device_id))):
self.dev = bus.get_object('org.bluez', path)
break
else:
raise ValueError(f'Adapter {device_id} not found.')

self.adapter = dbus.Interface(self.dev, 'org.bluez.Adapter1')
# The sad news is someone decided that this convoluted mess passing
# strings back and forth to get properties would be simpler than literal
# adapter.some_property = 4 or adapter.some_property_set(4)
self.properties = dbus.Interface(self.dev, 'org.freedesktop.DBus.Properties')
self._adapter_name = self.dev.object_path.split("/")[-1]

def get_address(self) -> str:
"""
:returns adapter Bluetooth address
"""
return self.address
return str(self.properties.Get(self.adapter.dbus_interface, "Address"))

async def set_address(self, bt_addr, interactive=True):
if not interactive:
return False
# TODO: automated detection
print(f"Attempting to change the bluetooth MAC to {bt_addr}")
print("please choose your method:")
print("\t1: bdaddr - ericson, csr, TI, broadcom, zeevo, st")
print("\t2: hcitool - intel chipsets")
print("\t3: hcitool - cypress (raspberri pi 3B+ & 4B)")
print("\tx: abort, dont't change")
hci_version = " ".join(reversed(list(map(lambda h: '0x' + h, bt_addr.split(":")))))
c = input()
if c == '1':
await utils.run_system_command(f'bdaddr -i {self._adapter_name} {bt_addr}')
elif c == '2':
await utils.run_system_command(f'hcitool cmd 0x3f 0x0031 {hci_version}')
elif c == '3':
await utils.run_system_command(f'hcitool cmd 0x3f 0x001 {hci_version}')
else:
return False
await utils.run_system_command("hciconfig hci0 reset")
await utils.run_system_command("systemctl restart bluetooth.service")

# now we have to reget all dbus-shenanigans because we just restarted it's service.
self.__init__(self._device_id)

if self.get_address() != bt_addr:
logger.info("Failed to set btaddr")
return False
else:
logger.info(f"Changed bt_addr to {bt_addr}")
return True

def get_paired_switches(self):
switches = []
for path, ifaces in dbus.SystemBus().get_object('org.bluez', '/').GetManagedObjects('org.freedesktop.DBus.ObjectManager', dbus_interface='org.freedesktop.DBus.ObjectManager').items():
d = ifaces.get("org.bluez.Device1")
if d and d['Name'] == "Nintendo Switch":
switches += [path]
return switches

def unpair_path(self, path):
self.adapter.RemoveDevice(path)

def powered(self, boolean=True):
self.properties.Set(self.adapter.dbus_interface, 'Powered', boolean)
Expand All @@ -60,6 +103,8 @@ async def set_class(self, cls='0x002508'):
"""
logger.info(f'setting device class to {cls}...')
await utils.run_system_command(f'hciconfig {self._adapter_name} class {cls}')
if self.properties.Get(self.adapter.dbus_interface, "Class") != int(cls, base=0):
logger.error(f"Could not set class to the required {cls}. Connecting probably won't work.")

async def set_name(self, name: str):
"""
Expand All @@ -69,6 +114,9 @@ async def set_name(self, name: str):
logger.info(f'setting device name to {name}...')
self.properties.Set(self.adapter.dbus_interface, 'Alias', name)

def get_UUIDs(self):
return self.properties.Get(self.adapter.dbus_interface, "UUIDs")

@staticmethod
def register_sdp_record(record_path):
_uuid = str(uuid.uuid4())
Expand All @@ -85,4 +133,6 @@ def register_sdp_record(record_path):
manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")
manager.RegisterProfile(HID_PATH, _uuid, opts)

return _uuid
@staticmethod
def get_address_of_paired_path(path):
return str(dbus.SystemBus().get_object('org.bluez', path).Get('org.bluez.Device1', "Address", dbus_interface='org.freedesktop.DBus.Properties'))
Loading