Added button functionality and info
This commit is contained in:
parent
78e62fd013
commit
21f7f42cc9
14 changed files with 666 additions and 182 deletions
50
demo.py
50
demo.py
|
|
@ -3,13 +3,57 @@ import time
|
||||||
from nxbt import Nxbt
|
from nxbt import Nxbt
|
||||||
from nxbt import ControllerTypes
|
from nxbt import ControllerTypes
|
||||||
|
|
||||||
|
MACRO = """
|
||||||
|
B 0.1s
|
||||||
|
0.1s
|
||||||
|
B 0.1s
|
||||||
|
0.1s
|
||||||
|
B 0.1s
|
||||||
|
0.1s
|
||||||
|
B 0.1s
|
||||||
|
1.5s
|
||||||
|
DPAD_LEFT 0.1s
|
||||||
|
0.1s
|
||||||
|
DPAD_LEFT 0.1s
|
||||||
|
0.1s
|
||||||
|
DPAD_LEFT 0.1s
|
||||||
|
0.1s
|
||||||
|
DPAD_LEFT 0.1s
|
||||||
|
0.1s
|
||||||
|
DPAD_LEFT 0.1s
|
||||||
|
0.1s
|
||||||
|
DPAD_RIGHT 0.075s
|
||||||
|
0.075s
|
||||||
|
DPAD_RIGHT 0.075s
|
||||||
|
0.075s
|
||||||
|
DPAD_RIGHT 0.075s
|
||||||
|
0.075s
|
||||||
|
A 0.1s
|
||||||
|
1.5s
|
||||||
|
A 0.1s
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
nxbt = Nxbt()
|
nxbt = Nxbt()
|
||||||
|
adapters = nxbt.get_available_adapters()
|
||||||
index = nxbt.create_controller(
|
index = nxbt.create_controller(
|
||||||
ControllerTypes.PRO_CONTROLLER, "/org/bluez/hci0")
|
ControllerTypes.PRO_CONTROLLER,
|
||||||
|
adapters[0],
|
||||||
|
colour_body=[0xFF, 0x7B, 0x83],
|
||||||
|
colour_buttons=[0xFF, 0xF0, 0x78])
|
||||||
|
index2 = nxbt.create_controller(
|
||||||
|
ControllerTypes.PRO_CONTROLLER,
|
||||||
|
adapters[1],
|
||||||
|
colour_body=[0xFF, 0xFF, 0xFF],
|
||||||
|
colour_buttons=[0xFF, 0xF0, 0x78])
|
||||||
|
nxbt.macro(index2, MACRO, block=False)
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print(nxbt.get_state()[0])
|
state = nxbt.state[0]
|
||||||
|
if not state["errors"]:
|
||||||
|
print(state["finished_macros"])
|
||||||
|
else:
|
||||||
|
print(state["errors"])
|
||||||
|
break
|
||||||
|
|
|
||||||
93
docs/Analog Stick and Button Input.md
Normal file
93
docs/Analog Stick and Button Input.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Analog Stick and Button Input Information
|
||||||
|
|
||||||
|
**Disclaimer:** The info within this document is sourced from the Switch reverse engineering
|
||||||
|
effort at [DekuNukem's Repository](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering).
|
||||||
|
|
||||||
|
The below sections contain info on the formulation and derivation of data
|
||||||
|
pertaining to the Nintendo Switch's controllers. The section on the analog
|
||||||
|
sticks contains info on encoding/decoding stick X/Y data, deadzones,
|
||||||
|
maximum range, etc. The button info section contains info on how each
|
||||||
|
button's state is communicated.
|
||||||
|
|
||||||
|
## Analog Stick Information
|
||||||
|
|
||||||
|
Information on a controller's analog sticks is stored in three primary
|
||||||
|
locations (user calibration excluded):
|
||||||
|
|
||||||
|
| Obtained From | Byte # | Data Type | Info |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Standard Input Report | 6-11 | 2 uint16 | Contains X/Y Data of Analog Sticks<sup>1</sup>
|
||||||
|
| SPI Flash Read (Offset 0x6080) | 13-30 | 12 uint16 LE | Dead Zone, Range ratio |
|
||||||
|
| SPI Flash Read (Offset 0x603D) | 7-24 | 12 uint16 LE | X/Y Min/Max and Centers |
|
||||||
|
|
||||||
|
<sup>1</sup> This data is relative, meaning that stick calibration data
|
||||||
|
*must* be used to encode/decode X and Y positions.
|
||||||
|
|
||||||
|
## Decoding a Stick's Position
|
||||||
|
|
||||||
|
**Note:** The following configuration values are used within Nxbt.
|
||||||
|
|
||||||
|
First, we use the data obtained from the 0x603D SPI flash read to
|
||||||
|
derive the right/left stick calibration parameters.
|
||||||
|
|
||||||
|
Sample data output by Nxbt:
|
||||||
|
```
|
||||||
|
Payload: 0xA1 0x21 0x2B 0x90 0x00 0x00 0x00 0x74 0x58 0x75 0x4B 0x68 0x7C 0x90
|
||||||
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13
|
||||||
|
Subcommand: 0x90 0x10 0x3D 0x60 0x00 0x00 0x19 0xBA 0xF5 0x62 0x6F 0xC8 0x77 0xED
|
||||||
|
14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
||||||
|
0x95 0x5B 0x16 0xD8 0x7D 0xF2 0xB5 0x5F 0x86 0x65 0x5E 0xFF 0x82 0x82
|
||||||
|
28 29 30 31 32 33 34 35
|
||||||
|
0x82 0x0F 0x0F 0x0F 0x00 0x00 0x00 0x00
|
||||||
|
```
|
||||||
|
|
||||||
|
Which gives us:
|
||||||
|
|
||||||
|
```
|
||||||
|
Left Stick: 0xBA 0xF5 0x62 0x6F 0xC8 0x77 0xED 0x95 0x5B
|
||||||
|
Right Stick: 0x16 0xD8 0x7D 0xF2 0xB5 0x5F 0x86 0x65 0x5E
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the following equations, we can decode these values into meaningful ones.
|
||||||
|
Each stick's data is treated as an array of byte values for the equations.
|
||||||
|
|
||||||
|
```
|
||||||
|
# The nine stick bytes are labelled stick_cal[0] - stick_cal[8] here
|
||||||
|
uint16_t data[6]
|
||||||
|
data[0] = (stick_cal[1] << 8) & 0xF00 | stick_cal[0];
|
||||||
|
data[1] = (stick_cal[2] << 4) | (stick_cal[1] >> 4);
|
||||||
|
data[2] = (stick_cal[4] << 8) & 0xF00 | stick_cal[3];
|
||||||
|
data[3] = (stick_cal[5] << 4) | (stick_cal[4] >> 4);
|
||||||
|
data[4] = (stick_cal[7] << 8) & 0xF00 | stick_cal[6];
|
||||||
|
data[5] = (stick_cal[8] << 4) | (stick_cal[7] >> 4);
|
||||||
|
|
||||||
|
# These values used as such in, for example, a right stick
|
||||||
|
uint16_t rstick_center_x = data[0];
|
||||||
|
uint16_t rstick_center_y = data[1];
|
||||||
|
uint16_t rstick_x_min = rstick_center_x - data[2];
|
||||||
|
uint16_t rstick_x_max = rstick_center_x + data[4];
|
||||||
|
uint16_t rstick_y_min = rstick_center_y - data[3];
|
||||||
|
uint16_t rstick_y_max = rstick_center_y + data[5];
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting in the following values for the sticks:
|
||||||
|
|
||||||
|
```
|
||||||
|
Left Stick
|
||||||
|
~~~~~~~~~~
|
||||||
|
Center X =
|
||||||
|
Center Y =
|
||||||
|
X Min =
|
||||||
|
X Max =
|
||||||
|
Y Min =
|
||||||
|
Y Max =
|
||||||
|
|
||||||
|
Right Stick
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Center X =
|
||||||
|
Center Y =
|
||||||
|
X Min =
|
||||||
|
X Max =
|
||||||
|
Y Min =
|
||||||
|
Y Max =
|
||||||
|
```
|
||||||
|
|
@ -748,12 +748,13 @@ Subcommand: 0x90 0x10 0x3D 0x60 0x00 0x00 0x19 0xD5 0xA4 0x43 0xA4 0xE7 0x87 0x2
|
||||||
| 1 | Subcommand reply |
|
| 1 | Subcommand reply |
|
||||||
| 5-2 | SPI memory address (Little Endian) |
|
| 5-2 | SPI memory address (Little Endian) |
|
||||||
| 6 | Read length |
|
| 6 | Read length |
|
||||||
| 7-25 | Stick factory calibration |
|
| 7-24 | Stick factory calibration |
|
||||||
|
| 25 | Spacer byte (always 0xFF) |
|
||||||
| 26-31 | Controller colours |
|
| 26-31 | Controller colours |
|
||||||
|
|
||||||
#### Stick Factory Calibration
|
#### Stick Factory Calibration
|
||||||
|
|
||||||
Byte 7-15 comprises the left stick calibration data. Byte 16-25 comprises the
|
Byte 7-15 comprises the left stick calibration data. Byte 16-24 comprises the
|
||||||
right stick data. These 9 bytes are used to generate 6 uint16 values which are
|
right stick data. These 9 bytes are used to generate 6 uint16 values which are
|
||||||
used to calculate the stick dead zones and min/max x/y values.
|
used to calculate the stick dead zones and min/max x/y values.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ this project.
|
||||||
|
|
||||||
- Controller SDP record. They all share the same one (generally), so a only a single record is needed to emulate all three controllers
|
- Controller SDP record. They all share the same one (generally), so a only a single record is needed to emulate all three controllers
|
||||||
- The Bluetooth alias "Joy-Con (L)", "Joy-Con (R)", or "Pro Controller"
|
- The Bluetooth alias "Joy-Con (L)", "Joy-Con (R)", or "Pro Controller"
|
||||||
- The Bluetooth Gamepad HID Class
|
|
||||||
|
**Note:** Setting the device's major and minor class is *not* required
|
||||||
|
to get the Switch to connect. Only the alias and SDP record are required.
|
||||||
|
|
||||||
### Weird Tibit:
|
### Weird Tibit:
|
||||||
|
|
||||||
|
|
|
||||||
132
nxbt/bluez.py
132
nxbt/bluez.py
|
|
@ -89,19 +89,75 @@ def find_objects(bus, service_name, interface_name):
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_input_plugin(toggle):
|
||||||
|
"""Enables or disables the BlueZ input plugin. Requires
|
||||||
|
root user to be run. The units and Bluetooth service will
|
||||||
|
not be restarted if the input plugin already matches
|
||||||
|
the toggle.
|
||||||
|
|
||||||
|
:param toggle: A boolean element indicating if the plugin
|
||||||
|
is enabled (True) or disabled (False)
|
||||||
|
:type toggle: boolean
|
||||||
|
:raises PermissionError: If the user is not root
|
||||||
|
:raises Exception: If the units can't be reloaded
|
||||||
|
"""
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
raise PermissionError("The input plugin must be toggled as root")
|
||||||
|
|
||||||
|
service_path = "/lib/systemd/system/bluetooth.service"
|
||||||
|
service = None
|
||||||
|
with open(service_path, "r") as f:
|
||||||
|
service = f.read()
|
||||||
|
|
||||||
|
# Find the bluetooth service execution line
|
||||||
|
lines = service.split("\n")
|
||||||
|
for i in range(0, len(lines)):
|
||||||
|
line = lines[i]
|
||||||
|
if line.startswith("ExecStart="):
|
||||||
|
# If we want to ensure the plugin is enabled
|
||||||
|
if toggle:
|
||||||
|
# If input is already enabled
|
||||||
|
if "--noplugin=input" not in line:
|
||||||
|
return
|
||||||
|
lines[i] = re.sub(" --noplugin=input", "", line)
|
||||||
|
else:
|
||||||
|
# If input is already disabled
|
||||||
|
if "--noplugin=input" in line:
|
||||||
|
return
|
||||||
|
# If not, add the flag
|
||||||
|
lines[i] = line + " --noplugin=input"
|
||||||
|
|
||||||
|
service = "\n".join(lines)
|
||||||
|
with open(service_path, "w") as f:
|
||||||
|
f.write(service)
|
||||||
|
|
||||||
|
# Reload units
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "daemon-reload"],
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
cmd_err = result.stderr.decode("utf-8").replace("\n", "")
|
||||||
|
if cmd_err != "":
|
||||||
|
raise Exception(cmd_err)
|
||||||
|
|
||||||
|
# Reload the bluetooth service with input disabled
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "restart", "bluetooth"],
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
cmd_err = result.stderr.decode("utf-8").replace("\n", "")
|
||||||
|
if cmd_err != "":
|
||||||
|
raise Exception(cmd_err)
|
||||||
|
|
||||||
|
|
||||||
class BlueZ():
|
class BlueZ():
|
||||||
"""Exposes the BlueZ D-Bus API as a Python object.
|
"""Exposes the BlueZ D-Bus API as a Python object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device_id="hci0"):
|
def __init__(self, adapter_path="/org/bluez/hci0"):
|
||||||
self.bus = dbus.SystemBus()
|
self.bus = dbus.SystemBus()
|
||||||
|
self.device_path = adapter_path
|
||||||
# Try to find the default adapter (hci0) or a user specified adapter
|
|
||||||
self.device_path = find_object_path(
|
|
||||||
self.bus,
|
|
||||||
SERVICE_NAME,
|
|
||||||
ADAPTER_INTERFACE,
|
|
||||||
object_name=device_id)
|
|
||||||
|
|
||||||
# If we weren't able to find an adapter with the specified ID,
|
# If we weren't able to find an adapter with the specified ID,
|
||||||
# try to find any usable Bluetooth adapter
|
# try to find any usable Bluetooth adapter
|
||||||
|
|
@ -123,10 +179,7 @@ class BlueZ():
|
||||||
self.device_path),
|
self.device_path),
|
||||||
"org.freedesktop.DBus.Properties")
|
"org.freedesktop.DBus.Properties")
|
||||||
|
|
||||||
if device_id:
|
self.device_id = self.device_path.split("/")[-1]
|
||||||
self.device_id = device_id
|
|
||||||
else:
|
|
||||||
self.device_id = self.device_path.split("/")[-1]
|
|
||||||
|
|
||||||
# Load the ProfileManager interface
|
# Load the ProfileManager interface
|
||||||
self.profile_manager = dbus.Interface(self.bus.get_object(
|
self.profile_manager = dbus.Interface(self.bus.get_object(
|
||||||
|
|
@ -400,61 +453,6 @@ class BlueZ():
|
||||||
BLUEZ_OBJECT_PATH),
|
BLUEZ_OBJECT_PATH),
|
||||||
PROFILEMANAGER_INTERFACE)
|
PROFILEMANAGER_INTERFACE)
|
||||||
|
|
||||||
def toggle_input_plugin(self, toggle):
|
|
||||||
"""Enables or disables the BlueZ input plugin. Requires
|
|
||||||
root user to be run. The units and Bluetooth service will
|
|
||||||
not be restarted if the input plugin already matches
|
|
||||||
the toggle.
|
|
||||||
|
|
||||||
:param toggle: A boolean element indicating if the plugin
|
|
||||||
is enabled (True) or disabled (False)
|
|
||||||
:type toggle: boolean
|
|
||||||
:raises PermissionError: If the user is not root
|
|
||||||
:raises Exception: If the units can't be reloaded
|
|
||||||
"""
|
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise PermissionError("The input plugin must be toggled as root")
|
|
||||||
|
|
||||||
service_path = "/lib/systemd/system/bluetooth.service"
|
|
||||||
service = None
|
|
||||||
with open(service_path, "r") as f:
|
|
||||||
service = f.read()
|
|
||||||
|
|
||||||
# Find the bluetooth service execution line
|
|
||||||
lines = service.split("\n")
|
|
||||||
for i in range(0, len(lines)):
|
|
||||||
line = lines[i]
|
|
||||||
if line.startswith("ExecStart="):
|
|
||||||
# If we want to ensure the plugin is enabled
|
|
||||||
if toggle:
|
|
||||||
# If input is already enabled
|
|
||||||
if "--noplugin=input" not in line:
|
|
||||||
return
|
|
||||||
lines[i] = re.sub(" --noplugin=input", "", line)
|
|
||||||
else:
|
|
||||||
# If input is already disabled
|
|
||||||
if "--noplugin=input" in line:
|
|
||||||
return
|
|
||||||
# If not, add the flag
|
|
||||||
lines[i] = line + " --noplugin=input"
|
|
||||||
|
|
||||||
service = "\n".join(lines)
|
|
||||||
with open(service_path, "w") as f:
|
|
||||||
f.write(service)
|
|
||||||
|
|
||||||
# Reload units
|
|
||||||
result = subprocess.run(
|
|
||||||
["systemctl", "daemon-reload"],
|
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
cmd_err = result.stderr.decode("utf-8").replace("\n", "")
|
|
||||||
if cmd_err != "":
|
|
||||||
raise Exception(cmd_err)
|
|
||||||
|
|
||||||
# Reload the bluetooth service with input disabled
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def get_discovered_devices(self):
|
def get_discovered_devices(self):
|
||||||
"""Gets a dict of all discovered (or previously discovered
|
"""Gets a dict of all discovered (or previously discovered
|
||||||
and connected) devices. The key is the device's dbus object
|
and connected) devices. The key is the device's dbus object
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,4 @@ class Controller():
|
||||||
except dbus.exceptions.DBusException:
|
except dbus.exceptions.DBusException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.bt.set_device_class(self.GAMEPAD_CLASS)
|
# self.bt.set_device_class(self.GAMEPAD_CLASS)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,150 @@
|
||||||
|
from time import perf_counter
|
||||||
|
|
||||||
|
|
||||||
class InputParser():
|
class InputParser():
|
||||||
|
|
||||||
def __init__():
|
def __init__(self, protocol):
|
||||||
print("")
|
|
||||||
|
self.protocol = protocol
|
||||||
|
|
||||||
|
# Buffers a list of unparsed macros
|
||||||
|
self.macro_buffer = []
|
||||||
|
# Keeps track of the entire current
|
||||||
|
# list of macro commands.
|
||||||
|
self.current_macro = None
|
||||||
|
self.current_macro_id = None
|
||||||
|
# Keeps track of the macro commands being
|
||||||
|
# input over a period of time.
|
||||||
|
self.current_macro_commands = None
|
||||||
|
# The time length of the current macro
|
||||||
|
self.macro_timer_length = 0
|
||||||
|
# The start time for the current macro commands
|
||||||
|
self.macro_timer_start = 0
|
||||||
|
|
||||||
|
self.controller_input = None
|
||||||
|
|
||||||
|
def buffer_macro(self, macro, macro_id):
|
||||||
|
|
||||||
|
# Doesn't have any info
|
||||||
|
if len(macro) < 4:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.macro_buffer.append([macro, macro_id])
|
||||||
|
|
||||||
|
def set_controller_input(self, controller_input):
|
||||||
|
|
||||||
|
self.controller_input = controller_input
|
||||||
|
|
||||||
|
def set_protocol_input(self, state=None):
|
||||||
|
|
||||||
|
if self.controller_input:
|
||||||
|
self.parse_controller_input(self.controller_input)
|
||||||
|
self.controller_input = None
|
||||||
|
|
||||||
|
elif (self.macro_buffer or self.current_macro or
|
||||||
|
self.current_macro_commands):
|
||||||
|
# Check if we can start on a new macro.
|
||||||
|
if not self.current_macro and self.macro_buffer:
|
||||||
|
# Preprocess command lines of current macro
|
||||||
|
macro = self.macro_buffer.pop(0)
|
||||||
|
self.current_macro = macro[0].strip("\n")
|
||||||
|
self.current_macro = self.current_macro.split("\n")
|
||||||
|
self.current_macro_id = macro[1]
|
||||||
|
|
||||||
|
# Check if we can load the next set of commands
|
||||||
|
if not self.current_macro_commands and self.current_macro:
|
||||||
|
self.current_macro_commands = (
|
||||||
|
self.current_macro.pop(0).strip(" ").split(" "))
|
||||||
|
|
||||||
|
# Timing metadata extraction
|
||||||
|
timer_length = self.current_macro_commands[-1]
|
||||||
|
timer_length = timer_length[0:len(timer_length)-1]
|
||||||
|
self.macro_timer_length = float(timer_length)
|
||||||
|
self.macro_timer_start = perf_counter()
|
||||||
|
|
||||||
|
self.parse_macro_input(self.current_macro_commands)
|
||||||
|
|
||||||
|
# Check if we're done inputting the current command
|
||||||
|
time_delta = perf_counter() - self.macro_timer_start
|
||||||
|
if time_delta > self.macro_timer_length:
|
||||||
|
self.current_macro_commands = None
|
||||||
|
# Check if we're done the current macro
|
||||||
|
if not self.current_macro and state:
|
||||||
|
finished = state["finished_macros"]
|
||||||
|
finished.append(self.current_macro_id)
|
||||||
|
state["finished_macros"] = finished
|
||||||
|
|
||||||
|
def parse_controller_input(self, controller_input):
|
||||||
|
|
||||||
|
return controller_input
|
||||||
|
|
||||||
|
def parse_macro_input(self, macro_input):
|
||||||
|
|
||||||
|
# Checking if this is a wait macro command
|
||||||
|
if len(macro_input) < 2:
|
||||||
|
print("waiting")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Arrays representing the 3 button bytes in the
|
||||||
|
# standard input report as binary.
|
||||||
|
upper = ['0'] * 8
|
||||||
|
shared = ['0'] * 8
|
||||||
|
lower = ['0'] * 8
|
||||||
|
for i in range(0, len(macro_input)-1):
|
||||||
|
button = macro_input[i]
|
||||||
|
# Upper Byte
|
||||||
|
if button == "Y":
|
||||||
|
upper[7] = '1'
|
||||||
|
elif button == "X":
|
||||||
|
upper[6] = '1'
|
||||||
|
elif button == "B":
|
||||||
|
upper[5] = '1'
|
||||||
|
elif button == "A":
|
||||||
|
upper[4] = '1'
|
||||||
|
elif button == "SR":
|
||||||
|
upper[3] = '1'
|
||||||
|
elif button == "SL":
|
||||||
|
upper[2] = '1'
|
||||||
|
elif button == "R":
|
||||||
|
upper[1] = '1'
|
||||||
|
elif button == "ZR":
|
||||||
|
upper[0] = '1'
|
||||||
|
|
||||||
|
# Shared byte
|
||||||
|
elif button == "-":
|
||||||
|
shared[7] = '1'
|
||||||
|
elif button == "+":
|
||||||
|
shared[6] = '1'
|
||||||
|
elif button == "R_ANALOG_DOWN":
|
||||||
|
shared[5] = '1'
|
||||||
|
elif button == "L_ANALOG_DOWN":
|
||||||
|
shared[4] = '1'
|
||||||
|
elif button == "HOME":
|
||||||
|
shared[3] = '1'
|
||||||
|
elif button == "CAPTURE":
|
||||||
|
shared[2] = '1'
|
||||||
|
|
||||||
|
# Lower byte
|
||||||
|
elif button == "DPAD_DOWN":
|
||||||
|
lower[7] = '1'
|
||||||
|
elif button == "DPAD_UP":
|
||||||
|
lower[6] = '1'
|
||||||
|
elif button == "DPAD_RIGHT":
|
||||||
|
lower[5] = '1'
|
||||||
|
elif button == "DPAD_LEFT":
|
||||||
|
lower[4] = '1'
|
||||||
|
elif button == "SR":
|
||||||
|
lower[3] = '1'
|
||||||
|
elif button == "SL":
|
||||||
|
lower[2] = '1'
|
||||||
|
elif button == "L":
|
||||||
|
lower[1] = '1'
|
||||||
|
elif button == "ZL":
|
||||||
|
lower[0] = '1'
|
||||||
|
|
||||||
|
# Converting binary strings to ints
|
||||||
|
upper_byte = int("".join(upper), 2)
|
||||||
|
shared_byte = int("".join(shared), 2)
|
||||||
|
lower_byte = int("".join(lower), 2)
|
||||||
|
|
||||||
|
self.protocol.set_button_inputs(upper_byte, shared_byte, lower_byte)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ class ControllerProtocol():
|
||||||
}
|
}
|
||||||
VIBRATOR_BYTES = [0xA0, 0xB0, 0xC0, 0x90]
|
VIBRATOR_BYTES = [0xA0, 0xB0, 0xC0, 0x90]
|
||||||
|
|
||||||
def __init__(self, controller_type, bt_address, report_size=50):
|
def __init__(self, controller_type, bt_address, report_size=50,
|
||||||
|
colour_body=None, colour_buttons=None):
|
||||||
"""Initializes the protocol for the controller.
|
"""Initializes the protocol for the controller.
|
||||||
|
|
||||||
:param controller_type: The type of controller (Joy-Con (L),
|
:param controller_type: The type of controller (Joy-Con (L),
|
||||||
|
|
@ -52,6 +53,12 @@ class ControllerProtocol():
|
||||||
:type bt_address: string
|
:type bt_address: string
|
||||||
:param report_size: The size of the protocol report, defaults to 50
|
:param report_size: The size of the protocol report, defaults to 50
|
||||||
:type report_size: int, optional
|
:type report_size: int, optional
|
||||||
|
:param colour_body: Sets the body colour of the controller, defaults
|
||||||
|
to None
|
||||||
|
:type colour_body: list of bytes, optional
|
||||||
|
:param colour_buttons: Sets the colour of the controller buttons,
|
||||||
|
defaults to None
|
||||||
|
:type colour_buttons: list of bytes, optional
|
||||||
:raises ValueError: On unknown controller type
|
:raises ValueError: On unknown controller type
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -107,8 +114,14 @@ class ControllerProtocol():
|
||||||
|
|
||||||
# Controller colours
|
# Controller colours
|
||||||
# Body Colour
|
# Body Colour
|
||||||
self.colour_body = [0x82] * 3
|
if not colour_body:
|
||||||
self.colour_buttons = [0x0F] * 3
|
self.colour_body = [0x82] * 3
|
||||||
|
else:
|
||||||
|
self.colour_body = colour_body
|
||||||
|
if not colour_buttons:
|
||||||
|
self.colour_buttons = [0x0F] * 3
|
||||||
|
else:
|
||||||
|
self.colour_buttons = colour_buttons
|
||||||
|
|
||||||
def get_report(self):
|
def get_report(self):
|
||||||
|
|
||||||
|
|
@ -167,9 +180,11 @@ class ControllerProtocol():
|
||||||
|
|
||||||
# Bad Packet handling statements
|
# Bad Packet handling statements
|
||||||
elif message.response == SwitchResponses.UNKNOWN_SUBCOMMAND:
|
elif message.response == SwitchResponses.UNKNOWN_SUBCOMMAND:
|
||||||
|
# Currently set so that the controller ignores any unknown
|
||||||
|
# subcommands. This is better than sending a NACK response
|
||||||
|
# since we'd just get stuck in an infinite loop arguing
|
||||||
|
# with the Switch.
|
||||||
self.set_full_input_report()
|
self.set_full_input_report()
|
||||||
# self.set_subcommand_reply()
|
|
||||||
# self.set_unknown_subcommand(message.subcommand_id)
|
|
||||||
|
|
||||||
elif message.response == SwitchResponses.NO_DATA:
|
elif message.response == SwitchResponses.NO_DATA:
|
||||||
self.set_full_input_report()
|
self.set_full_input_report()
|
||||||
|
|
@ -257,6 +272,14 @@ class ControllerProtocol():
|
||||||
|
|
||||||
self.report[13] = self.vibrator_report
|
self.report[13] = self.vibrator_report
|
||||||
|
|
||||||
|
def set_button_inputs(self, upper, shared, lower):
|
||||||
|
|
||||||
|
self.report[4] = upper
|
||||||
|
self.report[5] = shared
|
||||||
|
self.report[6] = lower
|
||||||
|
|
||||||
|
print(self.report)
|
||||||
|
|
||||||
def set_device_info(self):
|
def set_device_info(self):
|
||||||
|
|
||||||
# ACK Reply
|
# ACK Reply
|
||||||
|
|
@ -436,13 +459,16 @@ class ControllerProtocol():
|
||||||
else:
|
else:
|
||||||
replace_subarray(self.report, 30, 9, value=0xFF)
|
replace_subarray(self.report, 30, 9, value=0xFF)
|
||||||
|
|
||||||
|
# Spacer byte
|
||||||
|
self.report[39] = 0xFF
|
||||||
|
|
||||||
# Body colour
|
# Body colour
|
||||||
replace_subarray(
|
replace_subarray(
|
||||||
self.report, 39, 3,
|
self.report, 40, 3,
|
||||||
replace_arr=self.colour_body)
|
replace_arr=self.colour_body)
|
||||||
# Buttons colour
|
# Buttons colour
|
||||||
replace_subarray(
|
replace_subarray(
|
||||||
self.report, 42, 3,
|
self.report, 43, 3,
|
||||||
replace_arr=self.colour_buttons)
|
replace_arr=self.colour_buttons)
|
||||||
|
|
||||||
# Six-Axis motion sensor factor calibration
|
# Six-Axis motion sensor factor calibration
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,41 @@ import socket
|
||||||
import fcntl
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
import queue
|
||||||
|
|
||||||
from .controller import Controller, ControllerTypes
|
from .controller import Controller, ControllerTypes
|
||||||
from ..bluez import BlueZ
|
from ..bluez import BlueZ
|
||||||
from .protocol import ControllerProtocol
|
from .protocol import ControllerProtocol
|
||||||
|
from .input import InputParser
|
||||||
from .utils import format_msg_controller, format_msg_switch
|
from .utils import format_msg_controller, format_msg_switch
|
||||||
|
|
||||||
|
|
||||||
class ControllerServer():
|
class ControllerServer():
|
||||||
|
|
||||||
def __init__(self, controller_type, bt_device_id="hci0"):
|
def __init__(self, controller_type, adapter_path="/org/bluez/hci0",
|
||||||
|
lock=None, colour_body=None, colour_buttons=None):
|
||||||
|
|
||||||
self.controller_type = controller_type
|
self.controller_type = controller_type
|
||||||
|
self.colour_body = colour_body
|
||||||
|
self.colour_buttons = colour_buttons
|
||||||
|
|
||||||
|
if lock:
|
||||||
|
self.lock = lock
|
||||||
|
|
||||||
|
self.reconnect_counter = 0
|
||||||
|
|
||||||
# Intializing Bluetooth
|
# Intializing Bluetooth
|
||||||
self.bt = BlueZ(device_id=bt_device_id)
|
self.bt = BlueZ(adapter_path=adapter_path)
|
||||||
|
|
||||||
self.controller = Controller(self.bt, self.controller_type)
|
self.controller = Controller(self.bt, self.controller_type)
|
||||||
self.protocol = ControllerProtocol(
|
self.protocol = ControllerProtocol(
|
||||||
self.controller_type,
|
self.controller_type,
|
||||||
self.bt.address)
|
self.bt.address,
|
||||||
|
colour_body=self.colour_body,
|
||||||
|
colour_buttons=self.colour_buttons)
|
||||||
|
|
||||||
|
self.input = InputParser(self.protocol)
|
||||||
|
|
||||||
def run(self, reconnect_address=None, state=None, task_queue=None):
|
def run(self, reconnect_address=None, state=None, task_queue=None):
|
||||||
"""Runs the mainloop of the controller server.
|
"""Runs the mainloop of the controller server.
|
||||||
|
|
@ -31,49 +46,107 @@ class ControllerServer():
|
||||||
:type reconnect_address: string, optional
|
:type reconnect_address: string, optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(reconnect_address, state, task_queue)
|
|
||||||
|
|
||||||
if state:
|
if state:
|
||||||
state["state"] = "initializing"
|
state["state"] = "initializing"
|
||||||
|
|
||||||
self.controller.setup()
|
try:
|
||||||
|
# If we have a lock, prevent other controllers
|
||||||
if reconnect_address:
|
# from initializing at the same time and saturating
|
||||||
itr, s_itr, ctrl, s_ctrl = self.reconnect(
|
# the DBus
|
||||||
reconnect_address, state=state)
|
if self.lock:
|
||||||
else:
|
self.lock.acquire()
|
||||||
itr, s_itr, ctrl, s_ctrl = self.connect(state=state)
|
|
||||||
|
|
||||||
if state:
|
|
||||||
state["state"] = "connected"
|
|
||||||
|
|
||||||
# Mainloop
|
|
||||||
while True:
|
|
||||||
# Attempt to get output from Switch
|
|
||||||
try:
|
try:
|
||||||
reply = itr.recv(50)
|
self.controller.setup()
|
||||||
if len(reply) > 40:
|
|
||||||
print(format_msg_switch(reply))
|
|
||||||
except BlockingIOError:
|
|
||||||
reply = None
|
|
||||||
|
|
||||||
self.protocol.process_commands(reply)
|
if reconnect_address:
|
||||||
msg = self.protocol.get_report()
|
itr, ctrl = self.reconnect(reconnect_address, state=state)
|
||||||
|
else:
|
||||||
|
itr, ctrl = self.connect(state=state)
|
||||||
|
except Exception:
|
||||||
|
if self.lock:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
if reply:
|
self.switch_address = itr.getsockname()[0]
|
||||||
print(format_msg_controller(msg))
|
|
||||||
|
|
||||||
try:
|
if state:
|
||||||
itr.sendall(msg)
|
state["state"] = "connected"
|
||||||
except BlockingIOError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Respond at 120Hz for Pro Controller
|
# Mainloop
|
||||||
# or 60Hz for Joy-Cons
|
while True:
|
||||||
if self.controller_type == ControllerTypes.PRO_CONTROLLER:
|
# Attempt to get output from Switch
|
||||||
time.sleep(1/120)
|
try:
|
||||||
|
reply = itr.recv(50)
|
||||||
|
if len(reply) > 40:
|
||||||
|
print(format_msg_switch(reply))
|
||||||
|
except BlockingIOError:
|
||||||
|
reply = None
|
||||||
|
|
||||||
|
# Getting any inputs from the task queue
|
||||||
|
if task_queue:
|
||||||
|
try:
|
||||||
|
msg = task_queue.get_nowait()
|
||||||
|
print(msg)
|
||||||
|
if msg:
|
||||||
|
self.input.buffer_macro(
|
||||||
|
msg["macro"], msg["macro_id"])
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.protocol.process_commands(reply)
|
||||||
|
self.input.set_protocol_input(state=state)
|
||||||
|
msg = self.protocol.get_report()
|
||||||
|
|
||||||
|
if reply:
|
||||||
|
print(format_msg_controller(msg))
|
||||||
|
|
||||||
|
try:
|
||||||
|
itr.sendall(msg)
|
||||||
|
except BlockingIOError:
|
||||||
|
continue
|
||||||
|
except OSError as e:
|
||||||
|
# Attempt to reconnect to the Switch
|
||||||
|
if self.reconnect_counter < 2:
|
||||||
|
try:
|
||||||
|
print("Attempting to reconnect")
|
||||||
|
# Reinitialize the protocol
|
||||||
|
self.protocol = ControllerProtocol(
|
||||||
|
self.controller_type,
|
||||||
|
self.bt.address,
|
||||||
|
colour_body=self.colour_body,
|
||||||
|
colour_buttons=self.colour_buttons)
|
||||||
|
itr, ctrl = self.reconnect(self.switch_address,
|
||||||
|
state=state)
|
||||||
|
except OSError:
|
||||||
|
self.reconnect_counter += 1
|
||||||
|
print(e)
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
# If we can't reconnect, transition to attempting
|
||||||
|
# to connect to any Switch.
|
||||||
|
else:
|
||||||
|
print("Connecting")
|
||||||
|
# Reinitialize the protocol
|
||||||
|
self.protocol = ControllerProtocol(
|
||||||
|
self.controller_type,
|
||||||
|
self.bt.address,
|
||||||
|
colour_body=self.colour_body,
|
||||||
|
colour_buttons=self.colour_buttons)
|
||||||
|
itr, ctrl = self.connect(state=state)
|
||||||
|
self.switch_address = itr.getsockname()[0]
|
||||||
|
|
||||||
|
# Respond at 120Hz for Pro Controller
|
||||||
|
# or 60Hz for Joy-Cons
|
||||||
|
if self.controller_type == ControllerTypes.PRO_CONTROLLER:
|
||||||
|
time.sleep(1/120)
|
||||||
|
else:
|
||||||
|
time.sleep(1/60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if state:
|
||||||
|
state["state"] = "crashed"
|
||||||
|
state["errors"] = traceback.format_exc()
|
||||||
else:
|
else:
|
||||||
time.sleep(1/60)
|
raise e
|
||||||
|
|
||||||
def connect(self, state=None):
|
def connect(self, state=None):
|
||||||
"""Configures as a specified controller, pairs with a Nintendo Switch,
|
"""Configures as a specified controller, pairs with a Nintendo Switch,
|
||||||
|
|
@ -132,8 +205,8 @@ class ControllerServer():
|
||||||
self.protocol.process_commands(reply)
|
self.protocol.process_commands(reply)
|
||||||
msg = self.protocol.get_report()
|
msg = self.protocol.get_report()
|
||||||
|
|
||||||
# if reply:
|
if reply:
|
||||||
# print(format_msg_controller(msg))
|
print(format_msg_controller(msg))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
itr.sendall(msg)
|
itr.sendall(msg)
|
||||||
|
|
@ -148,7 +221,7 @@ class ControllerServer():
|
||||||
# Pairing cycle responds optimally on a 15Hz loop
|
# Pairing cycle responds optimally on a 15Hz loop
|
||||||
time.sleep(1/15)
|
time.sleep(1/15)
|
||||||
|
|
||||||
return itr, s_itr, ctrl, s_ctrl
|
return itr, ctrl
|
||||||
|
|
||||||
def reconnect(self, reconnect_address, state=None):
|
def reconnect(self, reconnect_address, state=None):
|
||||||
"""Attempts to reconnect with a Switch at the given address.
|
"""Attempts to reconnect with a Switch at the given address.
|
||||||
|
|
@ -160,31 +233,30 @@ class ControllerServer():
|
||||||
if state:
|
if state:
|
||||||
state["state"] = "reconnecting"
|
state["state"] = "reconnecting"
|
||||||
|
|
||||||
device_path = self.bt.find_device_by_address(reconnect_address)
|
|
||||||
if not device_path:
|
|
||||||
raise ValueError(
|
|
||||||
"No device Switch found with MAC address " + reconnect_address)
|
|
||||||
|
|
||||||
# Creating control and interrupt sockets
|
# Creating control and interrupt sockets
|
||||||
s_ctrl = socket.socket(
|
ctrl = socket.socket(
|
||||||
family=socket.AF_BLUETOOTH,
|
family=socket.AF_BLUETOOTH,
|
||||||
type=socket.SOCK_SEQPACKET,
|
type=socket.SOCK_SEQPACKET,
|
||||||
proto=socket.BTPROTO_L2CAP)
|
proto=socket.BTPROTO_L2CAP)
|
||||||
s_itr = socket.socket(
|
itr = socket.socket(
|
||||||
family=socket.AF_BLUETOOTH,
|
family=socket.AF_BLUETOOTH,
|
||||||
type=socket.SOCK_SEQPACKET,
|
type=socket.SOCK_SEQPACKET,
|
||||||
proto=socket.BTPROTO_L2CAP)
|
proto=socket.BTPROTO_L2CAP)
|
||||||
|
|
||||||
# Setting up HID interrupt/control sockets
|
# Setting up HID interrupt/control sockets
|
||||||
s_ctrl.bind((self.bt.address, 17))
|
ctrl.connect((reconnect_address, 17))
|
||||||
s_itr.bind((self.bt.address, 19))
|
itr.connect((reconnect_address, 19))
|
||||||
|
|
||||||
s_itr.listen(1)
|
fcntl.fcntl(itr, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||||
s_ctrl.listen(1)
|
|
||||||
|
|
||||||
self.bt.connect_device(device_path)
|
# Send an empty input report to the Switch to prompt a reply
|
||||||
|
self.protocol.process_commands(None)
|
||||||
|
msg = self.protocol.get_report()
|
||||||
|
itr.sendall(msg)
|
||||||
|
|
||||||
ctrl, ctrl_address = s_ctrl.accept()
|
# Setting interrupt connection as non-blocking
|
||||||
itr, itr_address = s_itr.accept()
|
# In this case, non-blocking means it throws a "BlockingIOError"
|
||||||
|
# for sending and receiving, instead of blocking
|
||||||
|
fcntl.fcntl(itr, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||||
|
|
||||||
print("Here")
|
return itr, ctrl
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
def replace_subarray(arr, start, num_elms, value=0, replace_arr=None):
|
def replace_subarray(arr, start, num_elms, value=0, replace_arr=None):
|
||||||
|
"""Replaces a subsection within an array with another
|
||||||
|
set of values.
|
||||||
|
|
||||||
|
:param arr: The array to replace values within
|
||||||
|
:type arr: list
|
||||||
|
:param start: The starting index for replacement
|
||||||
|
:type start: int
|
||||||
|
:param num_elms: The number of elements to be replaced
|
||||||
|
:type num_elms: int
|
||||||
|
:param value: The value to replace elements within the
|
||||||
|
subarray with, defaults to 0
|
||||||
|
:type value: any, optional
|
||||||
|
:param replace_arr: A subarray to insert within
|
||||||
|
the passed array, defaults to None
|
||||||
|
:type replace_arr: list, optional
|
||||||
|
"""
|
||||||
|
|
||||||
if replace_arr:
|
if replace_arr:
|
||||||
arr[start:start + num_elms] = replace_arr
|
arr[start:start + num_elms] = replace_arr
|
||||||
|
|
|
||||||
108
nxbt/nxbt.py
108
nxbt/nxbt.py
|
|
@ -3,16 +3,19 @@ from multiprocessing import Queue, Manager
|
||||||
import queue
|
import queue
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import atexit
|
import atexit
|
||||||
|
import os
|
||||||
|
|
||||||
import dbus
|
import dbus
|
||||||
|
|
||||||
from .controller import ControllerServer
|
from .controller import ControllerServer
|
||||||
from .bluez import find_objects, SERVICE_NAME, ADAPTER_INTERFACE
|
from .bluez import find_objects, toggle_input_plugin
|
||||||
|
from .bluez import SERVICE_NAME, ADAPTER_INTERFACE
|
||||||
|
|
||||||
|
|
||||||
class NxbtCommands(Enum):
|
class NxbtCommands(Enum):
|
||||||
|
|
||||||
CREATE_CONTROLLER = 0
|
CREATE_CONTROLLER = 0
|
||||||
|
INPUT_MACRO = 1
|
||||||
|
|
||||||
|
|
||||||
class Nxbt():
|
class Nxbt():
|
||||||
|
|
@ -22,12 +25,16 @@ class Nxbt():
|
||||||
# Main queue for nbxt tasks
|
# Main queue for nbxt tasks
|
||||||
self.task_queue = Queue()
|
self.task_queue = Queue()
|
||||||
|
|
||||||
|
# Sychronizes bluetooth actions
|
||||||
|
self.__bluetooth_lock = Lock()
|
||||||
|
|
||||||
# Creates/manages shared resources
|
# Creates/manages shared resources
|
||||||
self.resource_manager = Manager()
|
self.resource_manager = Manager()
|
||||||
# Shared dictionary for viewing overall nxbt state.
|
# Shared dictionary for viewing overall nxbt state.
|
||||||
# Should only be read by threads and wrote to by
|
# Should only be read by threads and wrote to by
|
||||||
# the main nxbt multiprocessing process.
|
# the main nxbt multiprocessing process.
|
||||||
self.state = self.resource_manager.dict()
|
self.manager_state = self.resource_manager.dict()
|
||||||
|
self.manager_state_lock = Lock()
|
||||||
|
|
||||||
# Shared, controller management properties.
|
# Shared, controller management properties.
|
||||||
# The controller lock is used to sychronize use.
|
# The controller lock is used to sychronize use.
|
||||||
|
|
@ -35,13 +42,17 @@ class Nxbt():
|
||||||
self.__controller_counter = 0
|
self.__controller_counter = 0
|
||||||
self.__adapters_in_use = []
|
self.__adapters_in_use = []
|
||||||
|
|
||||||
|
# Disable the BlueZ input plugin so we can use the
|
||||||
|
# HID control/interrupt Bluetooth ports
|
||||||
|
toggle_input_plugin(False)
|
||||||
|
|
||||||
# Exit handler
|
# Exit handler
|
||||||
atexit.register(self.on_exit)
|
atexit.register(self.on_exit)
|
||||||
|
|
||||||
# Starting the nxbt worker process
|
# Starting the nxbt worker process
|
||||||
self.controllers = Process(
|
self.controllers = Process(
|
||||||
target=self.__command_manager,
|
target=self.__command_manager,
|
||||||
args=((self.task_queue), (self.state)))
|
args=((self.task_queue), (self.manager_state)))
|
||||||
# Disabling daemonization since we need to spawn
|
# Disabling daemonization since we need to spawn
|
||||||
# other controller processes, however, this means
|
# other controller processes, however, this means
|
||||||
# we need to cleanup on exit.
|
# we need to cleanup on exit.
|
||||||
|
|
@ -55,9 +66,12 @@ class Nxbt():
|
||||||
if hasattr(self, "controllers") and self.controllers.is_alive():
|
if hasattr(self, "controllers") and self.controllers.is_alive():
|
||||||
self.controllers.terminate()
|
self.controllers.terminate()
|
||||||
|
|
||||||
|
# Re-enable the BlueZ input plugin
|
||||||
|
toggle_input_plugin(True)
|
||||||
|
|
||||||
def __command_manager(self, task_queue, state):
|
def __command_manager(self, task_queue, state):
|
||||||
|
|
||||||
cm = ControllerManager(state)
|
cm = ControllerManager(state, self.__bluetooth_lock)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
@ -70,13 +84,43 @@ class Nxbt():
|
||||||
cm.create_controller(
|
cm.create_controller(
|
||||||
msg["arguments"]["controller_index"],
|
msg["arguments"]["controller_index"],
|
||||||
msg["arguments"]["controller_type"],
|
msg["arguments"]["controller_type"],
|
||||||
msg["arguments"]["adapter_path"])
|
msg["arguments"]["adapter_path"],
|
||||||
|
msg["arguments"]["colour_body"],
|
||||||
|
msg["arguments"]["colour_buttons"])
|
||||||
|
elif msg["command"] == NxbtCommands.INPUT_MACRO:
|
||||||
|
cm.input_macro(
|
||||||
|
msg["arguments"]["controller_index"],
|
||||||
|
msg["arguments"]["macro"],
|
||||||
|
msg["arguments"]["macro_id"])
|
||||||
|
|
||||||
def send_input(self, msg):
|
def macro(self, controller_index, macro, block=True):
|
||||||
|
|
||||||
self.task_queue.put(msg)
|
if controller_index not in self.manager_state.keys():
|
||||||
|
raise ValueError("Specified controller does not exist")
|
||||||
|
|
||||||
def create_controller(self, controller_type, adapter_path):
|
# Get a unique ID to identify the macro
|
||||||
|
# so we can check when the controller is done inputting it
|
||||||
|
macro_id = os.urandom(24).hex()
|
||||||
|
self.task_queue.put({
|
||||||
|
"command": NxbtCommands.INPUT_MACRO,
|
||||||
|
"arguments": {
|
||||||
|
"controller_index": controller_index,
|
||||||
|
"macro": macro,
|
||||||
|
"macro_id": macro_id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if block:
|
||||||
|
while True:
|
||||||
|
finished = (self.manager_state
|
||||||
|
[controller_index]["finished_macros"])
|
||||||
|
if macro_id in finished:
|
||||||
|
break
|
||||||
|
|
||||||
|
return macro_id
|
||||||
|
|
||||||
|
def create_controller(self, controller_type, adapter_path, block=True,
|
||||||
|
colour_body=None, colour_buttons=None):
|
||||||
|
|
||||||
if adapter_path not in self.get_available_adapters():
|
if adapter_path not in self.get_available_adapters():
|
||||||
raise ValueError("Specified adapter is unavailable")
|
raise ValueError("Specified adapter is unavailable")
|
||||||
|
|
@ -93,11 +137,21 @@ class Nxbt():
|
||||||
"controller_index": self.__controller_counter,
|
"controller_index": self.__controller_counter,
|
||||||
"controller_type": controller_type,
|
"controller_type": controller_type,
|
||||||
"adapter_path": adapter_path,
|
"adapter_path": adapter_path,
|
||||||
|
"colour_body": colour_body,
|
||||||
|
"colour_buttons": colour_buttons,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
controller_index = self.__controller_counter
|
controller_index = self.__controller_counter
|
||||||
self.__controller_counter += 1
|
self.__controller_counter += 1
|
||||||
self.__adapters_in_use.append(adapter_path)
|
self.__adapters_in_use.append(adapter_path)
|
||||||
|
|
||||||
|
if type(controller_index) == int:
|
||||||
|
while True:
|
||||||
|
if controller_index in self.manager_state.keys():
|
||||||
|
state = self.manager_state[controller_index]
|
||||||
|
if (state["state"] == "connecting" or
|
||||||
|
state["state"] == "reconnecting"):
|
||||||
|
break
|
||||||
finally:
|
finally:
|
||||||
self.__controller_lock.release()
|
self.__controller_lock.release()
|
||||||
pass
|
pass
|
||||||
|
|
@ -111,21 +165,23 @@ class Nxbt():
|
||||||
|
|
||||||
return adapters
|
return adapters
|
||||||
|
|
||||||
def get_state(self):
|
@property
|
||||||
|
def state(self):
|
||||||
|
|
||||||
return self.state
|
return self.manager_state
|
||||||
|
|
||||||
|
|
||||||
class ControllerManager():
|
class ControllerManager():
|
||||||
|
|
||||||
def __init__(self, state):
|
def __init__(self, state, lock):
|
||||||
|
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.lock = lock
|
||||||
self.controller_resources = Manager()
|
self.controller_resources = Manager()
|
||||||
self.controller_states = []
|
self.__controller_queues = {}
|
||||||
self.controller_queues = []
|
|
||||||
|
|
||||||
def create_controller(self, index, controller_type, adapter_path):
|
def create_controller(self, index, controller_type, adapter_path,
|
||||||
|
colour_body=None, colour_buttons=None):
|
||||||
|
|
||||||
controller_queue = Queue()
|
controller_queue = Queue()
|
||||||
|
|
||||||
|
|
@ -134,12 +190,28 @@ class ControllerManager():
|
||||||
controller_state["finished_macros"] = []
|
controller_state["finished_macros"] = []
|
||||||
controller_state["errors"] = False
|
controller_state["errors"] = False
|
||||||
|
|
||||||
self.state[index] = controller_state
|
self.__controller_queues[index] = controller_queue
|
||||||
# Get the last parameter of the path, AKA the ID
|
|
||||||
device_id = adapter_path.split("/")[-1]
|
|
||||||
|
|
||||||
server = ControllerServer(controller_type, bt_device_id=device_id)
|
self.state[index] = controller_state
|
||||||
|
|
||||||
|
server = ControllerServer(controller_type,
|
||||||
|
adapter_path=adapter_path,
|
||||||
|
lock=self.lock,
|
||||||
|
colour_body=colour_body,
|
||||||
|
colour_buttons=colour_buttons)
|
||||||
controller = Process(target=server.run, args=(
|
controller = Process(target=server.run, args=(
|
||||||
None, controller_state, controller_queue))
|
None, controller_state, controller_queue))
|
||||||
controller.daemon = True
|
controller.daemon = True
|
||||||
controller.start()
|
controller.start()
|
||||||
|
|
||||||
|
def input_macro(self, index, macro, macro_id):
|
||||||
|
|
||||||
|
# finished = self.state[index]["finished_macros"]
|
||||||
|
# finished.append(macro_id)
|
||||||
|
# self.state[index]["finished_macros"] = finished
|
||||||
|
|
||||||
|
self.__controller_queues[index].put({
|
||||||
|
"type": "macro",
|
||||||
|
"macro": macro,
|
||||||
|
"macro_id": macro_id
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
"DPAD_LEFT": gp["buttons"][14]["pressed"],
|
"DPAD_LEFT": gp["buttons"][14]["pressed"],
|
||||||
"DPAD_RIGHT": gp["buttons"][15]["pressed"],
|
"DPAD_RIGHT": gp["buttons"][15]["pressed"],
|
||||||
"HOME": gp["buttons"][16]["pressed"],
|
"HOME": gp["buttons"][16]["pressed"],
|
||||||
"SCREENSHOT": gp["buttons"][17]["pressed"],
|
"CAPTURE": gp["buttons"][17]["pressed"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import time
|
||||||
import fcntl
|
import fcntl
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
|
|
||||||
|
from nxbt import toggle_input_plugin
|
||||||
from nxbt import BlueZ
|
from nxbt import BlueZ
|
||||||
from nxbt import Controller
|
from nxbt import Controller
|
||||||
from nxbt import ControllerTypes
|
from nxbt import ControllerTypes
|
||||||
|
|
@ -92,15 +93,22 @@ def write_to_buffer(buffer, message, message_type):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Switch Controller Bluetooth MAC Address goes here
|
# Switch Controller Bluetooth MAC Address goes here
|
||||||
jc_MAC = "XX:XX:XX:XX:XX:XX"
|
jc_MAC = "7C:BB:8A:FA:41:3D"
|
||||||
# Specify the type of controller here
|
# Specify the type of controller here
|
||||||
controller_type = ControllerTypes.JOYCON_L
|
controller_type = ControllerTypes.PRO_CONTROLLER
|
||||||
|
if controller_type == ControllerTypes.JOYCON_L:
|
||||||
|
REPLY = JCL_REPLY02
|
||||||
|
elif controller_type == ControllerTypes.JOYCON_R:
|
||||||
|
REPLY = JCR_REPLY02
|
||||||
|
else:
|
||||||
|
REPLY = PRO_REPLY02
|
||||||
|
|
||||||
port_ctrl = 17
|
port_ctrl = 17
|
||||||
port_itr = 19
|
port_itr = 19
|
||||||
message_buffer = []
|
message_buffer = []
|
||||||
|
|
||||||
bt = BlueZ()
|
bt = BlueZ()
|
||||||
bt.toggle_input_plugin(False)
|
toggle_input_plugin(False)
|
||||||
|
|
||||||
controller = Controller(bt, controller_type)
|
controller = Controller(bt, controller_type)
|
||||||
|
|
||||||
|
|
@ -196,11 +204,11 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Sending Switch the proxy's device info
|
# Sending Switch the proxy's device info
|
||||||
if controller_type == ControllerTypes.JOYCON_R:
|
if controller_type == ControllerTypes.JOYCON_R:
|
||||||
client_interrupt.sendall(JCR_REPLY02)
|
client_interrupt.sendall(REPLY)
|
||||||
elif controller_type == ControllerTypes.JOYCON_L:
|
elif controller_type == ControllerTypes.JOYCON_L:
|
||||||
client_interrupt.sendall(JCL_REPLY02)
|
client_interrupt.sendall(REPLY)
|
||||||
elif controller_type == ControllerTypes.PRO_CONTROLLER:
|
elif controller_type == ControllerTypes.PRO_CONTROLLER:
|
||||||
client_interrupt.sendall(PRO_REPLY02)
|
client_interrupt.sendall(REPLY)
|
||||||
|
|
||||||
# Waste some cycles here until we get the controllers info.
|
# Waste some cycles here until we get the controllers info.
|
||||||
# We don't want to proxy the device's info to the Switch
|
# We don't want to proxy the device's info to the Switch
|
||||||
|
|
|
||||||
44
test.py
44
test.py
|
|
@ -1,23 +1,29 @@
|
||||||
import time
|
from ctypes import c_uint16
|
||||||
|
|
||||||
from nxbt import ControllerTypes
|
# Left Stick Calibration
|
||||||
from nxbt import ControllerProtocol
|
stick_cal = [0xBA, 0xF5, 0x62, 0x6F, 0xC8, 0x77, 0xED, 0x95, 0x5B]
|
||||||
|
# Right Stick Calibration
|
||||||
|
stick_cal = [0x16, 0xD8, 0x7D, 0xF2, 0xB5, 0x5F, 0x86, 0x65, 0x5E]
|
||||||
|
data = [0] * 6
|
||||||
|
|
||||||
|
# The nine stick bytes are labelled stick_cal[0] - stick_cal[8] here
|
||||||
|
data[0] = (stick_cal[1] << 8) & 0xF00 | stick_cal[0]
|
||||||
|
data[1] = (stick_cal[2] << 4) | (stick_cal[1] >> 4)
|
||||||
|
data[2] = (stick_cal[4] << 8) & 0xF00 | stick_cal[3]
|
||||||
|
data[3] = (stick_cal[5] << 4) | (stick_cal[4] >> 4)
|
||||||
|
data[4] = (stick_cal[7] << 8) & 0xF00 | stick_cal[6]
|
||||||
|
data[5] = (stick_cal[8] << 4) | (stick_cal[7] >> 4)
|
||||||
|
|
||||||
INPUT_REPORT = b'\xa2\x01\x0E\x00\x00\x00\x00\x00\x00\x00\x00\x02\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
# These values used as such in, for example, a right stick
|
||||||
|
center_x = data[0]
|
||||||
|
center_y = data[1]
|
||||||
|
x_min = c_uint16(center_x - data[2])
|
||||||
|
x_max = c_uint16(center_x + data[4])
|
||||||
|
y_min = c_uint16(center_y - data[3])
|
||||||
|
y_max = c_uint16(center_y + data[5])
|
||||||
|
center_x = c_uint16(data[0])
|
||||||
|
center_y = c_uint16(data[1])
|
||||||
|
|
||||||
protocol = ControllerProtocol(
|
print("Center X and Y", center_x, center_y)
|
||||||
ControllerTypes.JOYCON_L,
|
print("X Min/Max", x_min, x_max)
|
||||||
"AA:AA:AA:AA:AA:AA")
|
print("Y Min/Max", y_min, y_max)
|
||||||
protocol.process_commands(None)
|
|
||||||
print(hex(protocol.get_report()[2]))
|
|
||||||
time.sleep(1)
|
|
||||||
protocol.process_commands(None)
|
|
||||||
print(hex(protocol.get_report()[2]))
|
|
||||||
protocol.process_commands(INPUT_REPORT)
|
|
||||||
print(hex(protocol.get_report()[2]))
|
|
||||||
time.sleep(1)
|
|
||||||
protocol.process_commands(None)
|
|
||||||
print(hex(protocol.get_report()[2]))
|
|
||||||
protocol.process_commands(None)
|
|
||||||
print(hex(protocol.get_report()[2]))
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue