diff --git a/demo.py b/demo.py index 429b28b..cefce9b 100644 --- a/demo.py +++ b/demo.py @@ -1,8 +1,7 @@ import time from random import randint -from nxbt import Nxbt -from nxbt import ControllerTypes +import nxbt MACRO = """ B 0.1s @@ -13,25 +12,44 @@ 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 +DPAD_DOWN 1.0s A 0.1s +0.25s +DPAD_DOWN 0.8s +A 0.1s +0.25s +L_STICK_PRESS 0.1s +1.0s +L_STICK@-100+000 0.75s +L_STICK@+000+100 0.75s +L_STICK@+100+000 0.75s +L_STICK@+000-100 0.75s +B 0.1s +0.25s +R_STICK_PRESS 0.1s +1.0s +R_STICK@-100+000 0.75s +R_STICK@+000+100 0.75s +R_STICK@+100+000 0.75s +R_STICK@+000-100 0.75s +B 0.1s +0.1s +B 0.1s +0.1s +B 0.1s +0.1s +B 0.1s +0.4s +DPAD_LEFT 0.1s +0.1s +A 0.1s +1.5s +A 0.1s +0.1s """ @@ -48,25 +66,26 @@ if __name__ == "__main__": # Loop over all Bluetooth adapters and create # Switch Pro Controllers - nxbt = Nxbt() - adapters = nxbt.get_available_adapters() - # adapters = ["/org/bluez/hci0"] + nx = nxbt.Nxbt() + adapters = nx.get_available_adapters() controller_idxs = [] for i in range(0, len(adapters)): - index = nxbt.create_controller( - ControllerTypes.PRO_CONTROLLER, + index = nx.create_controller( + nxbt.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) + # and don't wait for the macro to complete + nx.macro(controller_idxs[-1], MACRO, block=False) # Check the state while True: time.sleep(1) - for key in nxbt.state.keys(): - state = nxbt.state[key] + for key in nx.state.keys(): + state = nx.state[key] if not state["errors"]: print(state) else: diff --git a/nxbt/__init__.py b/nxbt/__init__.py index 29862d1..861c321 100644 --- a/nxbt/__init__.py +++ b/nxbt/__init__.py @@ -1,8 +1,10 @@ from .controller import ControllerServer -from .controller import ControllerTypes from .controller import ControllerProtocol from .controller import SwitchReportParser from .controller import SwitchResponses from .controller import Controller from .bluez import * from .nxbt import Nxbt +from .nxbt import JOYCON_L +from .nxbt import JOYCON_R +from .nxbt import PRO_CONTROLLER diff --git a/nxbt/cli.py b/nxbt/cli.py index fe717b6..8e51634 100644 --- a/nxbt/cli.py +++ b/nxbt/cli.py @@ -1,5 +1,124 @@ +import argparse +import time +from random import randint + from .web import start_web_app +from .nxbt import Nxbt, PRO_CONTROLLER + + +parser = argparse.ArgumentParser() +parser.add_argument('command', default=False, choices=['start', 'demo'], + help="Specifies the Nxbt command to run") +args = parser.parse_args() + + +MACRO = """ +LOOP 100 + B 0.1s + 0.1s + B 0.1s + 0.1s + B 0.1s + 0.1s + B 0.1s + 1.5s + DPAD_RIGHT 0.075s + 0.075s + A 0.1s + 1.5s + DPAD_DOWN 1.0s + A 0.1s + 0.25s + DPAD_DOWN 0.8s + A 0.1s + 0.25s + L_STICK_PRESS 0.1s + 1.0s + L_STICK@-100+000 0.75s + L_STICK@+000+100 0.75s + L_STICK@+100+000 0.75s + L_STICK@+000-100 0.75s + B 0.1s + 0.25s + R_STICK_PRESS 0.1s + 1.0s + R_STICK@-100+000 0.75s + R_STICK@+000+100 0.75s + R_STICK@+100+000 0.75s + R_STICK@+000-100 0.75s + B 0.1s + 0.1s + B 0.1s + 0.1s + B 0.1s + 0.1s + B 0.1s + 0.4s + DPAD_LEFT 0.1s + 0.1s + A 0.1s + 1.5s + A 0.1s + 20.0s +""" + +MACRO = """ +B 0.1s +3.0s +A 0.1s +3.0s +20.0s +B 0.1s +LOOP 100 + B 0.1s + 0.1s +""" + + +def random_colour(): + + return [ + randint(0, 255), + randint(0, 255), + randint(0, 255), + ] + + +def demo(): + """Loops over all available Bluetooth adapters + and creates controllers on each. The last available adapter + is used to run a macro. + """ + + nx = Nxbt() + adapters = nx.get_available_adapters() + controller_idxs = [] + for i in range(0, len(adapters)): + index = nx.create_controller( + PRO_CONTROLLER, + adapters[i], + colour_body=random_colour(), + colour_buttons=random_colour()) + controller_idxs.append(index) + + # Run a macro on the last controller + # and don't wait for the macro to complete + nx.macro(controller_idxs[-1], MACRO, block=False) + + # Check the state + while True: + time.sleep(1) + for key in nx.state.keys(): + state = nx.state[key] + if not state["errors"]: + print(state) + else: + print(state["errors"]) def main(): - start_web_app() + + if args.command == 'start': + start_web_app() + elif args.command == 'demo': + demo() diff --git a/nxbt/controller/input.py b/nxbt/controller/input.py index 37fe82a..8b8950b 100644 --- a/nxbt/controller/input.py +++ b/nxbt/controller/input.py @@ -3,12 +3,34 @@ from time import perf_counter class InputParser(): + # Left Stick calibration values + LEFT_STICK_CALIBRATION = { + "center_x": 2159, + "center_y": 1916, + # Zeroed Min/Max X and Y + "min_x": -1466, + "max_x": 1517, + "min_y": -1583, + "max_y": 1465, + } + # Right Stick calibration values + RIGHT_STICK_CALIBRATION = { + "center_x": 2070, + "center_y": 2013, + # Zeroed Min/Max X and Y + "min_x": -1522, + "max_x": 1414, + "min_y": -1531, + "max_y": 1510, + } + def __init__(self, protocol): 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 @@ -16,8 +38,10 @@ class InputParser(): # 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 @@ -47,8 +71,7 @@ class InputParser(): 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 = self.parse_macro(macro[0]) self.current_macro_id = macro[1] # Check if we can load the next set of commands @@ -62,6 +85,7 @@ class InputParser(): self.macro_timer_length = float(timer_length) self.macro_timer_start = perf_counter() + print(self.current_macro_commands) self.parse_macro_input(self.current_macro_commands) # Check if we're done inputting the current command @@ -78,6 +102,57 @@ class InputParser(): return controller_input + def parse_macro(self, macro): + + parsed = macro.split("\n") + parsed = list(filter(lambda s: not s.strip() == "", parsed)) + parsed = self.parse_loops(parsed) + + return parsed + + def parse_loops(self, macro): + parsed = [] + i = 0 + while i < len(macro): + line = macro[i] + if line.startswith("LOOP"): + loop_count = int(line.split(" ")[1]) + loop_buffer = [] + + # Detect delimiter and record + if macro[i+1].startswith("\t"): + loop_delimiter = "\t" + elif macro[i+1].startswith(" "): + loop_delimiter = " " + else: + loop_delimiter = " " + + # Gather looping commands + for j in range(i+1, len(macro)): + loop_line = macro[j] + if loop_line.startswith(loop_delimiter): + # Replace the first instance of the delimiter + loop_line = loop_line.replace(loop_delimiter, "", 1) + loop_buffer.append(loop_line) + # Set the new position if we either encounter the end + # of the loop or we reach the end of the macro + else: + i = j - 1 + break + if j+1 >= len(macro): + i = j + + # Recursively gather other loops if present + if any(s.startswith("LOOP") for s in loop_buffer): + loop_buffer = self.parse_loops(loop_buffer) + # Multiply out the loop and concatenate + parsed = parsed + (loop_buffer * loop_count) + else: + parsed.append(line) + i += 1 + + return parsed + def parse_macro_input(self, macro_input): # Checking if this is a wait macro command @@ -89,6 +164,9 @@ class InputParser(): upper = ['0'] * 8 shared = ['0'] * 8 lower = ['0'] * 8 + # Analog stick byte placeholders + stick_left = None + stick_right = None for i in range(0, len(macro_input)-1): button = macro_input[i] # Upper Byte @@ -114,9 +192,9 @@ class InputParser(): shared[7] = '1' elif button == "+": shared[6] = '1' - elif button == "R_ANALOG_DOWN": + elif button == "R_STICK_PRESS": shared[5] = '1' - elif button == "L_ANALOG_DOWN": + elif button == "L_STICK_PRESS": shared[4] = '1' elif button == "HOME": shared[3] = '1' @@ -141,9 +219,84 @@ class InputParser(): elif button == "ZL": lower[0] = '1' + # Analog Stick Positions + elif button.startswith("L_STICK@"): + stick_left = self.parse_macro_stick_position(button) + elif button.startswith("R_STICK@"): + stick_right = self.parse_macro_stick_position(button) + # 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) + if stick_left: + self.protocol.set_left_stick_inputs(stick_left) + if stick_right: + self.protocol.set_right_stick_inputs(stick_right) + + def parse_macro_stick_position(self, stick_pos): + + stick_type = stick_pos.split("@")[0] + positions = stick_pos.split("@")[1] + if len(positions) < 8: + return None + + # Converting macro to proper ratios + sign_x = positions[0] + ratio_x = int(positions[1:4]) / 100 + if sign_x == "-": + ratio_x = ratio_x * -1 + + sign_y = positions[4] + ratio_y = int(positions[5:8]) / 100 + if sign_y == "-": + ratio_y = ratio_y * -1 + + calibrated_position = self.stick_ratio_to_calibrated_position( + ratio_x, ratio_y, stick_type) + + return calibrated_position + + def stick_ratio_to_calibrated_position(self, ratio_x, ratio_y, stick_type): + + # Using the appropriate calibration values for the stick type + if stick_type == "L_STICK": + cal = self.LEFT_STICK_CALIBRATION + else: + cal = self.RIGHT_STICK_CALIBRATION + + # Converting ratios to uint16 values + if ratio_x < 0: + data_x_converted = ( + abs(ratio_x) * cal["min_x"] + cal["center_x"]) + else: + data_x_converted = ( + abs(ratio_x) * cal["max_x"] + cal["center_x"]) + data_x_converted = int(round(data_x_converted)) + + if ratio_y < 0: + data_y_converted = ( + abs(ratio_y) * cal["min_y"] + cal["center_y"]) + else: + data_y_converted = ( + abs(ratio_y) * cal["max_y"] + cal["center_y"]) + data_y_converted = int(round(data_y_converted)) + + # Converting the two X/Y uint16 values to 3 uint8 Little Endian values + # using bitshifting techniques + converted_values = [ + # Get the last two hex digits + data_x_converted & 0xFF, + # Combine the last digit of the Y uint16 and the first digit + # of the X uint16 + ((data_y_converted & 0xF) << 4) + (data_x_converted >> 8), + # Get the first two digits of the Y uint16 + data_y_converted >> 4] + + return converted_values + + def reassign_protocol(self, protocol): + + self.protocol = protocol diff --git a/nxbt/controller/protocol.py b/nxbt/controller/protocol.py index 819be8b..b69ca6d 100644 --- a/nxbt/controller/protocol.py +++ b/nxbt/controller/protocol.py @@ -3,7 +3,7 @@ import random from time import perf_counter from .controller import ControllerTypes -from .utils import replace_subarray +from .utils import replace_subarray, format_msg_controller class SwitchResponses(Enum): @@ -97,19 +97,19 @@ class ControllerProtocol(): # Disable left stick if we have a right Joy-Con if self.controller_type == ControllerTypes.JOYCON_R: - self.left_stick_status = [0x00] * 3 + self.left_stick_centre = [0x00] * 3 else: # Center values which are also reported under # SPI Stick calibration reads - self.left_stick_status = [0x6F, 0xC8, 0x77] + self.left_stick_centre = [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 + self.right_stick_centre = [0x00] * 3 else: # Center values which are also reported under # SPI Stick calibration reads - self.right_stick_status = [0x16, 0xD8, 0x7D] + self.right_stick_centre = [0x16, 0xD8, 0x7D] self.vibrator_report = random.choice(self.VIBRATOR_BYTES) @@ -130,6 +130,7 @@ class ControllerProtocol(): def get_report(self): report = bytes(self.report) + print(format_msg_controller(report)) # Clear report self.set_empty_report() return report @@ -266,22 +267,35 @@ class ControllerProtocol(): self.report[5] = self.button_status[1] self.report[6] = self.button_status[2] - self.report[7] = self.left_stick_status[0] - self.report[8] = self.left_stick_status[1] - self.report[9] = self.left_stick_status[2] + self.report[7] = self.left_stick_centre[0] + self.report[8] = self.left_stick_centre[1] + self.report[9] = self.left_stick_centre[2] - self.report[10] = self.right_stick_status[0] - self.report[11] = self.right_stick_status[1] - self.report[12] = self.right_stick_status[2] + self.report[10] = self.right_stick_centre[0] + self.report[11] = self.right_stick_centre[1] + self.report[12] = self.right_stick_centre[2] self.report[13] = self.vibrator_report def set_button_inputs(self, upper, shared, lower): + print(upper, shared, lower) self.report[4] = upper self.report[5] = shared self.report[6] = lower + def set_left_stick_inputs(self, left): + + self.report[7] = left[0] + self.report[8] = left[1] + self.report[9] = left[2] + + def set_right_stick_inputs(self, right): + + self.report[10] = right[0] + self.report[11] = right[1] + self.report[12] = right[2] + def set_device_info(self): # ACK Reply diff --git a/nxbt/controller/server.py b/nxbt/controller/server.py index 9679efb..2dfd8a6 100644 --- a/nxbt/controller/server.py +++ b/nxbt/controller/server.py @@ -104,7 +104,6 @@ class ControllerServer(): if self.task_queue: try: msg = self.task_queue.get_nowait() - print(msg) if msg: self.input.buffer_macro( msg["macro"], msg["macro_id"]) @@ -115,8 +114,8 @@ class ControllerServer(): self.input.set_protocol_input(state=self.state) msg = self.protocol.get_report() - if reply: - print(format_msg_controller(msg)) + # if reply: + # print(format_msg_controller(msg)) try: itr.sendall(msg) @@ -168,6 +167,7 @@ class ControllerServer(): self.bt.address, colour_body=self.colour_body, colour_buttons=self.colour_buttons) + self.input.reassign_protocol(self.protocol) if self.lock: self.lock.acquire() diff --git a/nxbt/nxbt.py b/nxbt/nxbt.py index 9c61543..7083380 100644 --- a/nxbt/nxbt.py +++ b/nxbt/nxbt.py @@ -8,10 +8,16 @@ import os import dbus from .controller import ControllerServer +from .controller import ControllerTypes from .bluez import find_objects, toggle_input_plugin from .bluez import SERVICE_NAME, ADAPTER_INTERFACE +JOYCON_L = ControllerTypes.JOYCON_L +JOYCON_R = ControllerTypes.JOYCON_R +PRO_CONTROLLER = ControllerTypes.PRO_CONTROLLER + + class NxbtCommands(Enum): CREATE_CONTROLLER = 0 @@ -147,7 +153,7 @@ class Nxbt(): # Block until the controller is ready # This needs to be done to prevent race conditions - # on DBus resources. + # on Bluetooth resources. if type(controller_index) == int: while True: if controller_index in self.manager_state.keys(): @@ -157,7 +163,6 @@ class Nxbt(): break finally: self.__controller_lock.release() - pass return controller_index diff --git a/scripts/proxy.py b/scripts/proxy.py index 0a3e8ee..a056855 100644 --- a/scripts/proxy.py +++ b/scripts/proxy.py @@ -15,7 +15,7 @@ from time import perf_counter from nxbt import toggle_input_plugin from nxbt import BlueZ from nxbt import Controller -from nxbt import ControllerTypes +from nxbt import JOYCON_L, JOYCON_R, PRO_CONTROLLER JCL_REPLY02 = b'\xA2\x21\x05\x8E\x84\x00\x12\x01\x18\x80\x01\x18\x80\x80\x82\x02\x03\x48\x01\x02\xDC\xA6\x32\x16\x4A\x7C\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -95,10 +95,10 @@ if __name__ == "__main__": # Switch Controller Bluetooth MAC Address goes here jc_MAC = "7C:BB:8A:FA:41:3D" # Specify the type of controller here - controller_type = ControllerTypes.PRO_CONTROLLER - if controller_type == ControllerTypes.JOYCON_L: + controller_type = PRO_CONTROLLER + if controller_type == JOYCON_L: REPLY = JCL_REPLY02 - elif controller_type == ControllerTypes.JOYCON_R: + elif controller_type == JOYCON_R: REPLY = JCR_REPLY02 else: REPLY = PRO_REPLY02 diff --git a/scripts/sticks.py b/scripts/sticks.py index bf717b6..d2d38d6 100644 --- a/scripts/sticks.py +++ b/scripts/sticks.py @@ -24,10 +24,10 @@ 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] +left_x_min = (left_center_x - data_left[0]) - left_center_x +left_x_max = (left_center_x + data_left[4]) - left_center_x +left_y_min = (left_center_y - data_left[1]) - left_center_y +left_y_max = (left_center_y + data_left[5]) - left_center_y print("Left Stick Values:") print("~~~~~~~~~~~~~~~~~~") @@ -38,10 +38,10 @@ 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] +right_x_min = (right_center_x - data_right[2]) - right_center_x +right_x_max = (right_center_x + data_right[4]) - right_center_x +right_y_min = (right_center_y - data_right[3]) - right_center_y +right_y_max = (right_center_y + data_right[5]) - right_center_y print("\nRight Stick Values:") print("~~~~~~~~~~~~~~~~~~~") @@ -63,9 +63,20 @@ 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) +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)) -data_y_converted = (abs(ratio_y) * (left_y_min - left_center_y) + left_center_y) + +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) diff --git a/setup.cfg b/setup.cfg index f47d1ce..227fb72 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,5 +43,5 @@ filterwarnings = error [flake8] -max-line-length = 80 +max-line-length = 100 exclude = .git, .eggs, __pycache__, tests/, docs/, build/, dist/ \ No newline at end of file