Fixed dual controller support

This commit is contained in:
Brikwerk 2020-06-02 22:21:48 -07:00
commit a4bce53d1e
13 changed files with 469 additions and 238 deletions

48
demo.py
View file

@ -1,4 +1,5 @@
import time
from random import randint
from nxbt import Nxbt
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__":
# Loop over all Bluetooth adapters and create
# Switch Pro Controllers
nxbt = Nxbt()
adapters = nxbt.get_available_adapters()
index = nxbt.create_controller(
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)
# adapters = ["/org/bluez/hci0"]
controller_idxs = []
for i in range(0, len(adapters)):
index = nxbt.create_controller(
ControllerTypes.PRO_CONTROLLER,
adapters[i],
colour_body=random_colour(),
colour_buttons=random_colour())
controller_idxs.append(index)
# Run a macro on the last controller
nxbt.macro(controller_idxs[-1], MACRO, block=False)
# Check the state
while True:
time.sleep(1)
state = nxbt.state[0]
if not state["errors"]:
print(state["finished_macros"])
else:
print(state["errors"])
break
for key in nxbt.state.keys():
state = nxbt.state[key]
if not state["errors"]:
print(state)
else:
print(state["errors"])

197
docs/Analog Stick Input.md Normal file
View 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']
```

View file

@ -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 =
```

View file

@ -21,3 +21,22 @@ inquiry input report packet.
Eg: You could get away with emulating a Joy-Con (L) while having the
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.

View file

@ -150,6 +150,9 @@ def toggle_input_plugin(toggle):
if cmd_err != "":
raise Exception(cmd_err)
# Kill a bit of time here to ensure all services have restarted
time.sleep(0.5)
class BlueZ():
"""Exposes the BlueZ D-Bus API as a Python object.

View file

@ -65,5 +65,3 @@ class Controller():
self.bt.register_profile(self.SDP_RECORD_PATH, self.SDP_UUID, opts)
except dbus.exceptions.DBusException:
pass
# self.bt.set_device_class(self.GAMEPAD_CLASS)

View file

@ -82,7 +82,6 @@ class InputParser():
# Checking if this is a wait macro command
if len(macro_input) < 2:
print("waiting")
return
# Arrays representing the 3 button bytes in the

View file

@ -99,13 +99,17 @@ class ControllerProtocol():
if self.controller_type == ControllerTypes.JOYCON_R:
self.left_stick_status = [0x00] * 3
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
if self.controller_type == ControllerTypes.JOYCON_L:
self.right_stick_status = [0x00] * 3
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)
@ -278,8 +282,6 @@ class ControllerProtocol():
self.report[5] = shared
self.report[6] = lower
print(self.report)
def set_device_info(self):
# ACK Reply

View file

@ -15,7 +15,19 @@ from .utils import format_msg_controller, format_msg_switch
class ControllerServer():
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.colour_body = colour_body
@ -38,7 +50,7 @@ class ControllerServer():
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.
:param reconnect_address: The Bluetooth MAC address of a
@ -46,115 +58,137 @@ class ControllerServer():
:type reconnect_address: string, optional
"""
if state:
state["state"] = "initializing"
self.state["state"] = "initializing"
try:
# If we have a lock, prevent other controllers
# from initializing at the same time and saturating
# the DBus
# from initializing at the same time and saturating the DBus,
# potentially causing a kernel panic.
if self.lock:
self.lock.acquire()
try:
self.controller.setup()
if reconnect_address:
itr, ctrl = self.reconnect(reconnect_address, state=state)
itr, ctrl = self.reconnect(reconnect_address)
else:
itr, ctrl = self.connect(state=state)
except Exception:
itr, ctrl = self.connect()
finally:
if self.lock:
self.lock.release()
self.switch_address = itr.getsockname()[0]
if state:
state["state"] = "connected"
self.state["state"] = "connected"
# Mainloop
while True:
# Attempt to get output from Switch
self.mainloop(itr, ctrl)
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:
reply = itr.recv(50)
if len(reply) > 40:
print(format_msg_switch(reply))
except BlockingIOError:
reply = None
msg = self.task_queue.get_nowait()
print(msg)
if msg:
self.input.buffer_macro(
msg["macro"], msg["macro_id"])
except queue.Empty:
pass
# 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=self.state)
msg = self.protocol.get_report()
self.protocol.process_commands(reply)
self.input.set_protocol_input(state=state)
msg = self.protocol.get_report()
if reply:
print(format_msg_controller(msg))
if reply:
print(format_msg_controller(msg))
try:
itr.sendall(msg)
except BlockingIOError:
continue
except OSError as e:
# Attempt to reconnect to the Switch
itr, ctrl = self.save_connection(e)
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()
# Respond at 120Hz for Pro Controller
# or 60Hz for Joy-Cons
if self.controller_type == ControllerTypes.PRO_CONTROLLER:
time.sleep(1/120)
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,
and creates/accepts sockets for communication with the Switch.
"""
if state:
state["state"] = "connecting"
self.state["state"] = "connecting"
# Creating control and interrupt sockets
s_ctrl = socket.socket(
@ -223,15 +257,14 @@ class ControllerServer():
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.
:param reconnect_address: The Bluetooth MAC address of the Switch
:type reconnect_address: string
"""
if state:
state["state"] = "reconnecting"
self.state["state"] = "reconnecting"
# Creating control and interrupt sockets
ctrl = socket.socket(

View file

@ -119,7 +119,7 @@ class Nxbt():
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):
if adapter_path not in self.get_available_adapters():
@ -145,6 +145,9 @@ class Nxbt():
self.__controller_counter += 1
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:
while True:
if controller_index in self.manager_state.keys():
@ -197,10 +200,11 @@ class ControllerManager():
server = ControllerServer(controller_type,
adapter_path=adapter_path,
lock=self.lock,
state=controller_state,
task_queue=controller_queue,
colour_body=colour_body,
colour_buttons=colour_buttons)
controller = Process(target=server.run, args=(
None, controller_state, controller_queue))
controller = Process(target=server.run)
controller.daemon = True
controller.start()

View file

@ -107,8 +107,8 @@ if __name__ == "__main__":
port_itr = 19
message_buffer = []
bt = BlueZ()
toggle_input_plugin(False)
bt = BlueZ(adapter_path="/org/bluez/hci0")
controller = Controller(bt, controller_type)
@ -136,7 +136,7 @@ if __name__ == "__main__":
# Ensure we are paired/connected to the JC
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
for key in devices.keys():
print(devices[key]["Address"])
@ -291,3 +291,6 @@ if __name__ == "__main__":
switch_ctrl.close()
raise e
finally:
toggle_input_plugin(True)

81
scripts/sticks.py Normal file
View 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
View file

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