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

Feature: Amiibo NFC reading support #89

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
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
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# joycontrol

Branch: master->amiibo_edits->amiibo_writing->V12_fixes

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 data read

## Installation
- Install dependencies

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

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)
```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)

- setup bluetooth
- change MAC to be in Nintendos range (starts with 94:58:CB)
for raspi 3B+ and 4B you can use `sudo ./scrips/change_btaddr.sh`
rerun after every reboot
- disable SPD
change the `ExecStart` paramters in `/lib/systemd/system/bluetooth.service` to `ExecStart=/usr/lib/bluetooth/bluetoothd -C -P sap,input,avrcp`
- change alias
`sudo bluetoothctl system-alias 'Pro Controller'`
Joycons are untested yet, might work might not....

## Command line interface example
- Run the script
Expand All @@ -37,6 +46,9 @@ This will create a PRO_CONTROLLER instance waiting for the Switch to connect.

The Switch only pairs with new controllers in the "Change Grip/Order" menu.

After pairing the switch will most certanly just disconnect for some reason.
Use the reconnect option to avoid this afterwards.

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.

Expand All @@ -51,7 +63,7 @@ Call "help" to see a list of available commands.
- 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.
This causes flooding of packets and makes input after initial 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.
Expand All @@ -66,3 +78,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)

[V12 Issues thread](https://github.com/Poohl/joycontrol/issues/3)
4 changes: 3 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 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
76 changes: 62 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 Down Expand Up @@ -69,6 +112,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 +131,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