Fixed dual controller support
This commit is contained in:
parent
21f7f42cc9
commit
a4bce53d1e
13 changed files with 469 additions and 238 deletions
48
demo.py
48
demo.py
|
|
@ -1,4 +1,5 @@
|
||||||
import time
|
import time
|
||||||
|
from random import randint
|
||||||
|
|
||||||
from nxbt import Nxbt
|
from nxbt import Nxbt
|
||||||
from nxbt import ControllerTypes
|
from nxbt import ControllerTypes
|
||||||
|
|
@ -34,26 +35,39 @@ A 0.1s
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def random_colour():
|
||||||
|
|
||||||
|
return [
|
||||||
|
randint(0, 255),
|
||||||
|
randint(0, 255),
|
||||||
|
randint(0, 255),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
# Loop over all Bluetooth adapters and create
|
||||||
|
# Switch Pro Controllers
|
||||||
nxbt = Nxbt()
|
nxbt = Nxbt()
|
||||||
adapters = nxbt.get_available_adapters()
|
adapters = nxbt.get_available_adapters()
|
||||||
index = nxbt.create_controller(
|
# adapters = ["/org/bluez/hci0"]
|
||||||
ControllerTypes.PRO_CONTROLLER,
|
controller_idxs = []
|
||||||
adapters[0],
|
for i in range(0, len(adapters)):
|
||||||
colour_body=[0xFF, 0x7B, 0x83],
|
index = nxbt.create_controller(
|
||||||
colour_buttons=[0xFF, 0xF0, 0x78])
|
ControllerTypes.PRO_CONTROLLER,
|
||||||
index2 = nxbt.create_controller(
|
adapters[i],
|
||||||
ControllerTypes.PRO_CONTROLLER,
|
colour_body=random_colour(),
|
||||||
adapters[1],
|
colour_buttons=random_colour())
|
||||||
colour_body=[0xFF, 0xFF, 0xFF],
|
controller_idxs.append(index)
|
||||||
colour_buttons=[0xFF, 0xF0, 0x78])
|
# Run a macro on the last controller
|
||||||
nxbt.macro(index2, MACRO, block=False)
|
nxbt.macro(controller_idxs[-1], MACRO, block=False)
|
||||||
|
|
||||||
|
# Check the state
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
state = nxbt.state[0]
|
for key in nxbt.state.keys():
|
||||||
if not state["errors"]:
|
state = nxbt.state[key]
|
||||||
print(state["finished_macros"])
|
if not state["errors"]:
|
||||||
else:
|
print(state)
|
||||||
print(state["errors"])
|
else:
|
||||||
break
|
print(state["errors"])
|
||||||
|
|
|
||||||
197
docs/Analog Stick Input.md
Normal file
197
docs/Analog Stick Input.md
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
# Analog Stick Input Information
|
||||||
|
|
||||||
|
**Disclaimer:** A chunk 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.
|
||||||
|
|
||||||
|
If you want to tweak or check out the full stick decode/encode script,
|
||||||
|
please visit the *scripts/sticks.py* script.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# The nine stick bytes are labelled stick_cal[0] - stick_cal[8] here
|
||||||
|
data = [0] * 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);
|
||||||
|
|
||||||
|
# Using the above data to create right stick data
|
||||||
|
right_center_x = data[0];
|
||||||
|
right_center_y = data[1];
|
||||||
|
right_x_min = rstick_center_x - data[2];
|
||||||
|
right_x_max = rstick_center_x + data[4];
|
||||||
|
right_y_min = rstick_center_y - data[3];
|
||||||
|
right_y_max = rstick_center_y + data[5];
|
||||||
|
|
||||||
|
# or left stick data
|
||||||
|
left_center_x = data[2]
|
||||||
|
left_center_y = data[3]
|
||||||
|
left_x_min = left_center_x - data[0]
|
||||||
|
left_x_max = left_center_x + data[4]
|
||||||
|
left_y_min = left_center_y - data[1]
|
||||||
|
left_y_max = left_center_y + data[5]
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting in the following values for the sticks:
|
||||||
|
|
||||||
|
```
|
||||||
|
Right Stick
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Center X = 2070
|
||||||
|
Center Y = 2013
|
||||||
|
X Min = 548
|
||||||
|
X Max = 3484
|
||||||
|
Y Min = 482
|
||||||
|
Y Max = 3523
|
||||||
|
|
||||||
|
Left Stick
|
||||||
|
~~~~~~~~~~
|
||||||
|
Center X = 2159
|
||||||
|
Center Y = 1916
|
||||||
|
X Min = 693
|
||||||
|
X Max = 3676
|
||||||
|
Y Min = 333
|
||||||
|
Y Max = 3381
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that the left stick calibration data is decoded slightly
|
||||||
|
different than the right stick calibration data.
|
||||||
|
|
||||||
|
With the above calibration data, we can now decode a controller's
|
||||||
|
reported stick position:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Sample Stick Data Conversion:
|
||||||
|
stick_data = [0xB3, 0x32, 0x6C]
|
||||||
|
stick_horizontal = stick_data[0] | ((stick_data[1] & 0xF) << 8)
|
||||||
|
stick_vertical = (stick_data[1] >> 4) | (stick_data[2] << 4)
|
||||||
|
|
||||||
|
print("Example Left Stick Data to Ratio Conversion:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
|
||||||
|
print("Raw X/Y Uint16 Values:", stick_horizontal, stick_vertical)
|
||||||
|
ratio_x = abs((stick_horizontal - left_center_x)) / (left_x_min - left_center_x)
|
||||||
|
ratio_y = (stick_vertical - left_center_y) / (left_y_min - left_center_y)
|
||||||
|
print("Relative X/Y Values", ratio_x, ratio_y)
|
||||||
|
```
|
||||||
|
|
||||||
|
Which results in the ratios:
|
||||||
|
|
||||||
|
```
|
||||||
|
Example Left Stick Data to Ratio Conversion:
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Raw X/Y Uint16 Values: 691 1731
|
||||||
|
Relative X/Y Values -1.0013642564802183 0.11686670878079596
|
||||||
|
```
|
||||||
|
|
||||||
|
We can see from the above data that the stick is being pushed left horizontally with
|
||||||
|
very little vertical component.
|
||||||
|
|
||||||
|
## Converting Ratio-based Stick Position to a Calibrated Position
|
||||||
|
|
||||||
|
Given the stick calibration settings from the previous section,
|
||||||
|
we can convert a given set of X/Y stick ratios to a calibrated set
|
||||||
|
of values. This worked example will use the ratios defined before
|
||||||
|
(-1.00136 X and 0.116866 Y).
|
||||||
|
|
||||||
|
First, we need to convert our given ratios to the numeric range
|
||||||
|
defined by the calibration settings. Since we're using left stick ratios
|
||||||
|
for our example, our X values range from 693 - 3676 and our Y values range
|
||||||
|
from 333 - 3381. The following section of code demonstrates the math
|
||||||
|
behind this conversion.
|
||||||
|
|
||||||
|
```python
|
||||||
|
print("Example Left Stick Ratio to Data Conversion:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
|
||||||
|
if ratio_x < 0:
|
||||||
|
data_x_converted = (abs(ratio_x) * (left_x_min - left_center_x) + left_center_x)
|
||||||
|
else:
|
||||||
|
data_x_converted = (abs(ratio_x) * (left_x_max - left_center_x) + left_center_x)
|
||||||
|
data_x_converted = int(round(data_x_converted))
|
||||||
|
|
||||||
|
if ratio_y < 0:
|
||||||
|
data_y_converted = (abs(ratio_y) * (left_y_min - left_center_y) + left_center_y)
|
||||||
|
else:
|
||||||
|
data_y_converted = (abs(ratio_y) * (left_y_max - left_center_y) + left_center_y)
|
||||||
|
data_y_converted = int(round(data_y_converted))
|
||||||
|
|
||||||
|
print("X/Y Converted Values:", data_x_converted, data_y_converted)
|
||||||
|
```
|
||||||
|
|
||||||
|
Which results in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Example Left Stick Ratio to Data Conversion:
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
X/Y Converted Values: 691 1731
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the stick's X/Y position is broken up into 3 bytes in the standard input
|
||||||
|
report, we need to split these uint16 values into 3 uint8 values. The following
|
||||||
|
code accomplishes this split:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Converting the two X/Y uint16 values to 3 uint8 Little Endian values
|
||||||
|
converted_values = [
|
||||||
|
# Get the last two hex digits
|
||||||
|
hex(data_x_converted & 0xFF),
|
||||||
|
# Combine the last digit of the Y uint16 and the first digit
|
||||||
|
# of the X uint16
|
||||||
|
hex(((data_y_converted & 0xF) << 4) + (data_x_converted >> 8)),
|
||||||
|
# Get the first two digits of the Y uint16
|
||||||
|
hex(data_y_converted >> 4)]
|
||||||
|
print("Uint8 Converted Values:", converted_values)
|
||||||
|
```
|
||||||
|
|
||||||
|
Which results bytes ready to be sent to the Switch:
|
||||||
|
|
||||||
|
```
|
||||||
|
Uint8 Converted Values: ['0xb3', '0x32', '0x6c']
|
||||||
|
```
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# 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 =
|
|
||||||
```
|
|
||||||
|
|
@ -21,3 +21,22 @@ inquiry input report packet.
|
||||||
|
|
||||||
Eg: You could get away with emulating a Joy-Con (L) while having the
|
Eg: You could get away with emulating a Joy-Con (L) while having the
|
||||||
Bluetooth alias set to "Pro Controller".
|
Bluetooth alias set to "Pro Controller".
|
||||||
|
|
||||||
|
## Pro Controller Grip Colours
|
||||||
|
|
||||||
|
At the time of writing, grip colours are being read by the Switch, however,
|
||||||
|
they aren't being used to display the controller graphic. Eg: If the left
|
||||||
|
and right grip colours are set to white and the controller body is set to
|
||||||
|
black, the grip colours will be black. This is likely because Nintendo hasn't
|
||||||
|
produced any official Pro Controllers that feature a unique grip and body
|
||||||
|
colour.
|
||||||
|
|
||||||
|
Currently, grip colours are hardcoded for the official, black Pro Controller.
|
||||||
|
The black pro controller reports all white (or blank) grip colours, however,
|
||||||
|
the Switch displays a slightly lighter grey when the icon is displayed. Any
|
||||||
|
emulated controller can produce this grip colour if the body colour is set
|
||||||
|
to #323232, the button colour set to #FFFFFF and the grip colours are set to
|
||||||
|
#FFFFFF.
|
||||||
|
|
||||||
|
In the future, Nintendo may produce more Pro Controller colours, however,
|
||||||
|
at this point in time, setting the grip colour is not possible.
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,9 @@ def toggle_input_plugin(toggle):
|
||||||
if cmd_err != "":
|
if cmd_err != "":
|
||||||
raise Exception(cmd_err)
|
raise Exception(cmd_err)
|
||||||
|
|
||||||
|
# Kill a bit of time here to ensure all services have restarted
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
class BlueZ():
|
class BlueZ():
|
||||||
"""Exposes the BlueZ D-Bus API as a Python object.
|
"""Exposes the BlueZ D-Bus API as a Python object.
|
||||||
|
|
|
||||||
|
|
@ -65,5 +65,3 @@ class Controller():
|
||||||
self.bt.register_profile(self.SDP_RECORD_PATH, self.SDP_UUID, opts)
|
self.bt.register_profile(self.SDP_RECORD_PATH, self.SDP_UUID, opts)
|
||||||
except dbus.exceptions.DBusException:
|
except dbus.exceptions.DBusException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# self.bt.set_device_class(self.GAMEPAD_CLASS)
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ class InputParser():
|
||||||
|
|
||||||
# Checking if this is a wait macro command
|
# Checking if this is a wait macro command
|
||||||
if len(macro_input) < 2:
|
if len(macro_input) < 2:
|
||||||
print("waiting")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Arrays representing the 3 button bytes in the
|
# Arrays representing the 3 button bytes in the
|
||||||
|
|
|
||||||
|
|
@ -99,13 +99,17 @@ class ControllerProtocol():
|
||||||
if self.controller_type == ControllerTypes.JOYCON_R:
|
if self.controller_type == ControllerTypes.JOYCON_R:
|
||||||
self.left_stick_status = [0x00] * 3
|
self.left_stick_status = [0x00] * 3
|
||||||
else:
|
else:
|
||||||
self.left_stick_status = [0x74, 0x58, 0x75]
|
# Center values which are also reported under
|
||||||
|
# SPI Stick calibration reads
|
||||||
|
self.left_stick_status = [0x6F, 0xC8, 0x77]
|
||||||
|
|
||||||
# Disable right stick if we have a left Joy-Con
|
# Disable right stick if we have a left Joy-Con
|
||||||
if self.controller_type == ControllerTypes.JOYCON_L:
|
if self.controller_type == ControllerTypes.JOYCON_L:
|
||||||
self.right_stick_status = [0x00] * 3
|
self.right_stick_status = [0x00] * 3
|
||||||
else:
|
else:
|
||||||
self.right_stick_status = [0x4B, 0x68, 0x7C]
|
# Center values which are also reported under
|
||||||
|
# SPI Stick calibration reads
|
||||||
|
self.right_stick_status = [0x16, 0xD8, 0x7D]
|
||||||
|
|
||||||
self.vibrator_report = random.choice(self.VIBRATOR_BYTES)
|
self.vibrator_report = random.choice(self.VIBRATOR_BYTES)
|
||||||
|
|
||||||
|
|
@ -278,8 +282,6 @@ class ControllerProtocol():
|
||||||
self.report[5] = shared
|
self.report[5] = shared
|
||||||
self.report[6] = lower
|
self.report[6] = lower
|
||||||
|
|
||||||
print(self.report)
|
|
||||||
|
|
||||||
def set_device_info(self):
|
def set_device_info(self):
|
||||||
|
|
||||||
# ACK Reply
|
# ACK Reply
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,19 @@ from .utils import format_msg_controller, format_msg_switch
|
||||||
class ControllerServer():
|
class ControllerServer():
|
||||||
|
|
||||||
def __init__(self, controller_type, adapter_path="/org/bluez/hci0",
|
def __init__(self, controller_type, adapter_path="/org/bluez/hci0",
|
||||||
lock=None, colour_body=None, colour_buttons=None):
|
state=None, task_queue=None, lock=None, colour_body=None,
|
||||||
|
colour_buttons=None):
|
||||||
|
|
||||||
|
if state:
|
||||||
|
self.state = state
|
||||||
|
else:
|
||||||
|
self.state = {
|
||||||
|
"state": "",
|
||||||
|
"finished_macros": [],
|
||||||
|
"errors": None
|
||||||
|
}
|
||||||
|
|
||||||
|
self.task_queue = task_queue
|
||||||
|
|
||||||
self.controller_type = controller_type
|
self.controller_type = controller_type
|
||||||
self.colour_body = colour_body
|
self.colour_body = colour_body
|
||||||
|
|
@ -38,7 +50,7 @@ class ControllerServer():
|
||||||
|
|
||||||
self.input = InputParser(self.protocol)
|
self.input = InputParser(self.protocol)
|
||||||
|
|
||||||
def run(self, reconnect_address=None, state=None, task_queue=None):
|
def run(self, reconnect_address=None):
|
||||||
"""Runs the mainloop of the controller server.
|
"""Runs the mainloop of the controller server.
|
||||||
|
|
||||||
:param reconnect_address: The Bluetooth MAC address of a
|
:param reconnect_address: The Bluetooth MAC address of a
|
||||||
|
|
@ -46,115 +58,137 @@ class ControllerServer():
|
||||||
:type reconnect_address: string, optional
|
:type reconnect_address: string, optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if state:
|
self.state["state"] = "initializing"
|
||||||
state["state"] = "initializing"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# If we have a lock, prevent other controllers
|
# If we have a lock, prevent other controllers
|
||||||
# from initializing at the same time and saturating
|
# from initializing at the same time and saturating the DBus,
|
||||||
# the DBus
|
# potentially causing a kernel panic.
|
||||||
if self.lock:
|
if self.lock:
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
self.controller.setup()
|
self.controller.setup()
|
||||||
|
|
||||||
if reconnect_address:
|
if reconnect_address:
|
||||||
itr, ctrl = self.reconnect(reconnect_address, state=state)
|
itr, ctrl = self.reconnect(reconnect_address)
|
||||||
else:
|
else:
|
||||||
itr, ctrl = self.connect(state=state)
|
itr, ctrl = self.connect()
|
||||||
except Exception:
|
finally:
|
||||||
if self.lock:
|
if self.lock:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
self.switch_address = itr.getsockname()[0]
|
self.switch_address = itr.getsockname()[0]
|
||||||
|
|
||||||
if state:
|
self.state["state"] = "connected"
|
||||||
state["state"] = "connected"
|
|
||||||
|
|
||||||
# Mainloop
|
self.mainloop(itr, ctrl)
|
||||||
while True:
|
|
||||||
# Attempt to get output from Switch
|
except Exception:
|
||||||
|
self.state["state"] = "crashed"
|
||||||
|
self.state["errors"] = traceback.format_exc()
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
def mainloop(self, itr, ctrl):
|
||||||
|
|
||||||
|
# Mainloop
|
||||||
|
while True:
|
||||||
|
# Attempt to get output from Switch
|
||||||
|
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 self.task_queue:
|
||||||
try:
|
try:
|
||||||
reply = itr.recv(50)
|
msg = self.task_queue.get_nowait()
|
||||||
if len(reply) > 40:
|
print(msg)
|
||||||
print(format_msg_switch(reply))
|
if msg:
|
||||||
except BlockingIOError:
|
self.input.buffer_macro(
|
||||||
reply = None
|
msg["macro"], msg["macro_id"])
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
# Getting any inputs from the task queue
|
self.protocol.process_commands(reply)
|
||||||
if task_queue:
|
self.input.set_protocol_input(state=self.state)
|
||||||
try:
|
msg = self.protocol.get_report()
|
||||||
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)
|
if reply:
|
||||||
self.input.set_protocol_input(state=state)
|
print(format_msg_controller(msg))
|
||||||
msg = self.protocol.get_report()
|
|
||||||
|
|
||||||
if reply:
|
try:
|
||||||
print(format_msg_controller(msg))
|
itr.sendall(msg)
|
||||||
|
except BlockingIOError:
|
||||||
|
continue
|
||||||
|
except OSError as e:
|
||||||
|
# Attempt to reconnect to the Switch
|
||||||
|
itr, ctrl = self.save_connection(e)
|
||||||
|
|
||||||
try:
|
# Respond at 120Hz for Pro Controller
|
||||||
itr.sendall(msg)
|
# or 60Hz for Joy-Cons
|
||||||
except BlockingIOError:
|
if self.controller_type == ControllerTypes.PRO_CONTROLLER:
|
||||||
continue
|
time.sleep(1/120)
|
||||||
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:
|
||||||
raise e
|
time.sleep(1/60)
|
||||||
|
|
||||||
def connect(self, state=None):
|
def save_connection(self, error, state=None):
|
||||||
|
|
||||||
|
while 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)
|
||||||
|
if self.lock:
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
itr, ctrl = self.reconnect(self.switch_address)
|
||||||
|
return itr, ctrl
|
||||||
|
finally:
|
||||||
|
if self.lock:
|
||||||
|
self.lock.release()
|
||||||
|
except OSError:
|
||||||
|
self.reconnect_counter += 1
|
||||||
|
print(error)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# If we can't reconnect, transition to attempting
|
||||||
|
# to connect to any Switch.
|
||||||
|
print("Connecting")
|
||||||
|
self.reconnect_counter = 0
|
||||||
|
|
||||||
|
# Reinitialize the protocol
|
||||||
|
self.protocol = ControllerProtocol(
|
||||||
|
self.controller_type,
|
||||||
|
self.bt.address,
|
||||||
|
colour_body=self.colour_body,
|
||||||
|
colour_buttons=self.colour_buttons)
|
||||||
|
|
||||||
|
if self.lock:
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
itr, ctrl = self.connect()
|
||||||
|
finally:
|
||||||
|
if self.lock:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
self.state["state"] = "connected"
|
||||||
|
|
||||||
|
self.switch_address = itr.getsockname()[0]
|
||||||
|
|
||||||
|
return itr, ctrl
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
"""Configures as a specified controller, pairs with a Nintendo Switch,
|
"""Configures as a specified controller, pairs with a Nintendo Switch,
|
||||||
and creates/accepts sockets for communication with the Switch.
|
and creates/accepts sockets for communication with the Switch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if state:
|
self.state["state"] = "connecting"
|
||||||
state["state"] = "connecting"
|
|
||||||
|
|
||||||
# Creating control and interrupt sockets
|
# Creating control and interrupt sockets
|
||||||
s_ctrl = socket.socket(
|
s_ctrl = socket.socket(
|
||||||
|
|
@ -223,15 +257,14 @@ class ControllerServer():
|
||||||
|
|
||||||
return itr, ctrl
|
return itr, ctrl
|
||||||
|
|
||||||
def reconnect(self, reconnect_address, state=None):
|
def reconnect(self, reconnect_address):
|
||||||
"""Attempts to reconnect with a Switch at the given address.
|
"""Attempts to reconnect with a Switch at the given address.
|
||||||
|
|
||||||
:param reconnect_address: The Bluetooth MAC address of the Switch
|
:param reconnect_address: The Bluetooth MAC address of the Switch
|
||||||
:type reconnect_address: string
|
:type reconnect_address: string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if state:
|
self.state["state"] = "reconnecting"
|
||||||
state["state"] = "reconnecting"
|
|
||||||
|
|
||||||
# Creating control and interrupt sockets
|
# Creating control and interrupt sockets
|
||||||
ctrl = socket.socket(
|
ctrl = socket.socket(
|
||||||
|
|
|
||||||
10
nxbt/nxbt.py
10
nxbt/nxbt.py
|
|
@ -119,7 +119,7 @@ class Nxbt():
|
||||||
|
|
||||||
return macro_id
|
return macro_id
|
||||||
|
|
||||||
def create_controller(self, controller_type, adapter_path, block=True,
|
def create_controller(self, controller_type, adapter_path,
|
||||||
colour_body=None, colour_buttons=None):
|
colour_body=None, colour_buttons=None):
|
||||||
|
|
||||||
if adapter_path not in self.get_available_adapters():
|
if adapter_path not in self.get_available_adapters():
|
||||||
|
|
@ -145,6 +145,9 @@ class Nxbt():
|
||||||
self.__controller_counter += 1
|
self.__controller_counter += 1
|
||||||
self.__adapters_in_use.append(adapter_path)
|
self.__adapters_in_use.append(adapter_path)
|
||||||
|
|
||||||
|
# Block until the controller is ready
|
||||||
|
# This needs to be done to prevent race conditions
|
||||||
|
# on DBus resources.
|
||||||
if type(controller_index) == int:
|
if type(controller_index) == int:
|
||||||
while True:
|
while True:
|
||||||
if controller_index in self.manager_state.keys():
|
if controller_index in self.manager_state.keys():
|
||||||
|
|
@ -197,10 +200,11 @@ class ControllerManager():
|
||||||
server = ControllerServer(controller_type,
|
server = ControllerServer(controller_type,
|
||||||
adapter_path=adapter_path,
|
adapter_path=adapter_path,
|
||||||
lock=self.lock,
|
lock=self.lock,
|
||||||
|
state=controller_state,
|
||||||
|
task_queue=controller_queue,
|
||||||
colour_body=colour_body,
|
colour_body=colour_body,
|
||||||
colour_buttons=colour_buttons)
|
colour_buttons=colour_buttons)
|
||||||
controller = Process(target=server.run, args=(
|
controller = Process(target=server.run)
|
||||||
None, controller_state, controller_queue))
|
|
||||||
controller.daemon = True
|
controller.daemon = True
|
||||||
controller.start()
|
controller.start()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@ if __name__ == "__main__":
|
||||||
port_itr = 19
|
port_itr = 19
|
||||||
message_buffer = []
|
message_buffer = []
|
||||||
|
|
||||||
bt = BlueZ()
|
|
||||||
toggle_input_plugin(False)
|
toggle_input_plugin(False)
|
||||||
|
bt = BlueZ(adapter_path="/org/bluez/hci0")
|
||||||
|
|
||||||
controller = Controller(bt, controller_type)
|
controller = Controller(bt, controller_type)
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Ensure we are paired/connected to the JC
|
# Ensure we are paired/connected to the JC
|
||||||
print("Attempting to re-pair with device")
|
print("Attempting to re-pair with device")
|
||||||
devices = bt.discover_devices(alias="Joy-Con (L)", timeout=8)
|
devices = bt.discover_devices(alias="Pro Controller", timeout=8)
|
||||||
jc_device_path = None
|
jc_device_path = None
|
||||||
for key in devices.keys():
|
for key in devices.keys():
|
||||||
print(devices[key]["Address"])
|
print(devices[key]["Address"])
|
||||||
|
|
@ -291,3 +291,6 @@ if __name__ == "__main__":
|
||||||
switch_ctrl.close()
|
switch_ctrl.close()
|
||||||
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
finally:
|
||||||
|
toggle_input_plugin(True)
|
||||||
|
|
|
||||||
81
scripts/sticks.py
Normal file
81
scripts/sticks.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Left Stick Calibration
|
||||||
|
stick_cal_left = [0xBA, 0xF5, 0x62, 0x6F, 0xC8, 0x77, 0xED, 0x95, 0x5B]
|
||||||
|
# Right Stick Calibration
|
||||||
|
stick_cal_right = [0x16, 0xD8, 0x7D, 0xF2, 0xB5, 0x5F, 0x86, 0x65, 0x5E]
|
||||||
|
data_left = [0] * 6
|
||||||
|
data_right = [0] * 6
|
||||||
|
|
||||||
|
# Left stick uint16 conversion
|
||||||
|
data_left[0] = (stick_cal_left[1] << 8) & 0xF00 | stick_cal_left[0]
|
||||||
|
data_left[1] = (stick_cal_left[2] << 4) | (stick_cal_left[1] >> 4)
|
||||||
|
data_left[2] = (stick_cal_left[4] << 8) & 0xF00 | stick_cal_left[3]
|
||||||
|
data_left[3] = (stick_cal_left[5] << 4) | (stick_cal_left[4] >> 4)
|
||||||
|
data_left[4] = (stick_cal_left[7] << 8) & 0xF00 | stick_cal_left[6]
|
||||||
|
data_left[5] = (stick_cal_left[8] << 4) | (stick_cal_left[7] >> 4)
|
||||||
|
|
||||||
|
# Right stick uint16 conversion
|
||||||
|
data_right[0] = (stick_cal_right[1] << 8) & 0xF00 | stick_cal_right[0]
|
||||||
|
data_right[1] = (stick_cal_right[2] << 4) | (stick_cal_right[1] >> 4)
|
||||||
|
data_right[2] = (stick_cal_right[4] << 8) & 0xF00 | stick_cal_right[3]
|
||||||
|
data_right[3] = (stick_cal_right[5] << 4) | (stick_cal_right[4] >> 4)
|
||||||
|
data_right[4] = (stick_cal_right[7] << 8) & 0xF00 | stick_cal_right[6]
|
||||||
|
data_right[5] = (stick_cal_right[8] << 4) | (stick_cal_right[7] >> 4)
|
||||||
|
|
||||||
|
# Left Stick Decode
|
||||||
|
left_center_x = data_left[2]
|
||||||
|
left_center_y = data_left[3]
|
||||||
|
left_x_min = left_center_x - data_left[0]
|
||||||
|
left_x_max = left_center_x + data_left[4]
|
||||||
|
left_y_min = left_center_y - data_left[1]
|
||||||
|
left_y_max = left_center_y + data_left[5]
|
||||||
|
|
||||||
|
print("Left Stick Values:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~")
|
||||||
|
print("Left Center X and Y:", left_center_x, left_center_y)
|
||||||
|
print("Left X Min/Max: ", left_x_min, "", left_x_max)
|
||||||
|
print("Left Y Min/Max: ", left_y_min, "", left_y_max)
|
||||||
|
|
||||||
|
# Right Stick Decode
|
||||||
|
right_center_x = data_right[0]
|
||||||
|
right_center_y = data_right[1]
|
||||||
|
right_x_min = right_center_x - data_right[2]
|
||||||
|
right_x_max = right_center_x + data_right[4]
|
||||||
|
right_y_min = right_center_y - data_right[3]
|
||||||
|
right_y_max = right_center_y + data_right[5]
|
||||||
|
|
||||||
|
print("\nRight Stick Values:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~~")
|
||||||
|
print("Right Center X and Y:", right_center_x, right_center_y)
|
||||||
|
print("Right X Min/Max: ", right_x_min, "", right_x_max)
|
||||||
|
print("Right Y Min/Max: ", right_y_min, "", right_y_max)
|
||||||
|
|
||||||
|
# Sample Stick Data Conversion:
|
||||||
|
stick_data = [0xB3, 0x32, 0x6C]
|
||||||
|
stick_horizontal = stick_data[0] | ((stick_data[1] & 0xF) << 8)
|
||||||
|
stick_vertical = (stick_data[1] >> 4) | (stick_data[2] << 4)
|
||||||
|
|
||||||
|
print("\nExample Left Stick Data to Ratio Conversion:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
|
||||||
|
print("Raw X/Y Uint16 Values:", stick_horizontal, stick_vertical)
|
||||||
|
ratio_x = abs((stick_horizontal - left_center_x)) / (left_x_min - left_center_x)
|
||||||
|
ratio_y = (stick_vertical - left_center_y) / (left_y_min - left_center_y)
|
||||||
|
print("Relative X/Y Values", ratio_x, ratio_y)
|
||||||
|
|
||||||
|
print("\nExample Left Stick Ratio to Data Conversion:")
|
||||||
|
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
|
||||||
|
data_x_converted = (abs(ratio_x) * (left_x_min - left_center_x) + left_center_x)
|
||||||
|
data_x_converted = int(round(data_x_converted))
|
||||||
|
data_y_converted = (abs(ratio_y) * (left_y_min - left_center_y) + left_center_y)
|
||||||
|
data_y_converted = int(round(data_y_converted))
|
||||||
|
print("X/Y Converted Values:", data_x_converted, data_y_converted)
|
||||||
|
|
||||||
|
# Converting the two X/Y uint16 values to 3 uint8 Little Endian values
|
||||||
|
converted_values = [
|
||||||
|
# Get the last two hex digits
|
||||||
|
hex(data_x_converted & 0xFF),
|
||||||
|
# Combine the last digit of the Y uint16 and the first digit
|
||||||
|
# of the X uint16
|
||||||
|
hex(((data_y_converted & 0xF) << 4) + (data_x_converted >> 8)),
|
||||||
|
# Get the first two digits of the Y uint16
|
||||||
|
hex(data_y_converted >> 4)]
|
||||||
|
print("Uint8 Converted Values:", converted_values)
|
||||||
29
test.py
29
test.py
|
|
@ -1,29 +0,0 @@
|
||||||
from ctypes import c_uint16
|
|
||||||
|
|
||||||
# Left Stick Calibration
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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])
|
|
||||||
|
|
||||||
print("Center X and Y", center_x, center_y)
|
|
||||||
print("X Min/Max", x_min, x_max)
|
|
||||||
print("Y Min/Max", y_min, y_max)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue