From 66e6654e4f22b54bb94f5e291cb79753f55344a2 Mon Sep 17 00:00:00 2001 From: Rolando Islas Date: Tue, 18 Apr 2017 05:24:25 -0700 Subject: [PATCH] Threads notify GUI on crash (+4 squashed commits) - Fixes #19 Cleaned up command responses a bit Add a method to send random audio to the Wii U Cleaned input controller Add command for micrphone input (random) --- drc-info.py | 61 ++++++---- drc-sim-backend.py | 16 +++ resources/command/na.json | 20 ++-- src/server/control/gamepad.py | 57 +++++++-- src/server/control/util/controller.py | 125 +++++++------------- src/server/data/constants.py | 3 +- src/server/data/struct/input.py | 10 +- src/server/net/server/command.py | 10 +- src/server/net/wii/audio.py | 29 +++++ src/server/net/wii/command.py | 4 +- src/server/ui/gui/frame/frame_run_server.py | 35 +++++- src/server/ui/gui/gui_main.py | 30 +++++ src/server/util/status_sending_thread.py | 33 ++++++ src/server/util/wpa_supplicant.py | 101 +++++++++++----- 14 files changed, 361 insertions(+), 173 deletions(-) create mode 100644 src/server/util/status_sending_thread.py diff --git a/drc-info.py b/drc-info.py index c92a700..f9be34e 100644 --- a/drc-info.py +++ b/drc-info.py @@ -6,7 +6,7 @@ from threading import Thread from src.server.data import constants -from src.server.data.struct import input +from src.server.data.struct import input, command sock_cmd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock_cmd.bind(("192.168.1.10", constants.PORT_WII_CMD)) @@ -22,44 +22,53 @@ def print_packet(sock, name): data = sock.recv(2048) - print("%s: %s" % (name, codecs.encode(data, "hex"))) + print("%s: %s" % (name, codecs.encode(data, "hex").decode())) def send_cmd(data): sock_cmd.sendto(data, ("192.168.1.11", constants.PORT_WII_CMD + 100)) +def send_command_from_string(command_string, sid): + send_data = command.header.parse(codecs.decode(command_string, "hex")) + send_data.seq_id = sid + if send_data.cmd_id == 1: + send_data.mic_enabled = 0 # floods logs with audio data if enabled + send_data = command.header.build(send_data) + send_cmd(send_data) + sid += 1 + sid %= 65535 + time.sleep(1) + return sid + + def cmd_request(): sid = 0 while True: data = { - 0: {0: {0: "000000000c00%s087e0115880040000000000000", - 10: "000000000d00%s007e0101780040000a0000000100"}, - 4: {4: "000000000c00%s007e0109780040040400000000", - 10: "000000000d00%s117e012fc80040040a0000000100", - 11: "000000000c00%s017e0107180040040b00000000"}, - 5: {6: "000000000c00%s007e0101a80040050600000000", - 12: "000000001100%s007e0102f80040050c000000050e0300870f", - 16: "000001003000%s80010000000000000000000000803e0000000100029e0000000000000070000000404003002d0000" + 0: {0: {0: "000000000c0005087e0115880040000000000000", + 10: "000000000d0005007e0101780040000a0000000100"}, + 4: {4: "000000000c0005007e0109780040040400000000", + 10: "000000000d0005117e012fc80040040a0000000100", + 11: "000000000c0005017e0107180040040b00000000"}, + 5: {6: "000000000c0005007e0101a80040050600000000", + 12: "00000000110005007e0102f80040050c000000050e0300870f", + 16: "0000010030000580010000000000000000000000803e0000000100029e0000000000000070000000404003002d0000" "018000400000000000", - 24: "000000001600%s007e0101c8004005180000000a54313936333030303030"} + 24: "00000000160005007e0101c8004005180000000a54313936333030303030"} }, - 1: {0: {0: "000001003000%s1a010000000000000000000000803e000000010002000000000000000070000000404003002d00000" - "10000000000000000" # Just CMD 1 - keys 0 0 are there so it fits nicely with the for loop - } - } + 1: "000001003000051a010000000000000000000000803e000000010002000000000000000070000000404003002d00000" + "10000000000000000" } - for command in data.keys(): - for primary_id in data[command].keys(): - for secondary_id in data[command][primary_id].keys(): - h = hex(sid).replace("0x", "") - if len(h) == 1: - h = "0" + h - send_data = bytes(codecs.decode(data[command][primary_id][secondary_id] % h, "hex")) - print("Sending command %d %d %d" % (command, primary_id, secondary_id)) - send_cmd(send_data) - sid += 1 - time.sleep(1) + for cmd in data.keys(): + if isinstance(data[cmd], str): + print("Sending command %d" % cmd) + sid = send_command_from_string(data[cmd], sid) + else: + for primary_id in data[cmd].keys(): + for secondary_id in data[cmd][primary_id].keys(): + print("Sending command %d %d %d" % (cmd, primary_id, secondary_id)) + sid = send_command_from_string(data[cmd][primary_id][secondary_id], sid) def print_hid(sock): diff --git a/drc-sim-backend.py b/drc-sim-backend.py index 24fbeee..0b52118 100644 --- a/drc-sim-backend.py +++ b/drc-sim-backend.py @@ -12,6 +12,10 @@ def init_loggers(): + """ + Initialize loggers with a specified log level if they have the argument. + :return: None + """ loggers = (Logger, LoggerBackend, LoggerGui, LoggerCli, LoggerWpa) for logger in loggers: if Args.args.debug: @@ -27,6 +31,10 @@ def init_loggers(): def start(): + """ + Main loop. It can be GUI or CLI based on args. Dies if an error makes it way here or main loop stops. + :return: None + """ ui = None try: if Args.args.cli: @@ -47,6 +55,10 @@ def start(): def log_level(): + """ + Log at every level to display the levels that are enabled. + :return: None + """ # Logger info Logger.debug("Debug logging enabled") Logger.extra("Extra debug logging enabled") @@ -57,6 +69,10 @@ def log_level(): def main(): + """ + Main entry point. Parses arguments, loads configuration files, initialized loggers and starts the main loop. + :return: None + """ Args.parse_args() ConfigServer.load() ConfigServer.save() diff --git a/resources/command/na.json b/resources/command/na.json index 2be2a8c..49caab8 100644 --- a/resources/command/na.json +++ b/resources/command/na.json @@ -1,22 +1,22 @@ { "0": { "0": { - "0": "0200000014000c087e0115880009000000000008190c0117190c0117", - "10": "02000000100007007e0101780009000a0000000400170200" + "0": "190c0117190c0117", + "10": "00170200" }, "3": { - "6": "000000000000000000000000000000000000000000" + "6": "00" }, "4": { - "4": "02000000280008007e010978000904040000001c0000000b00000040190c011728000058000800000c0221fe00001c58", - "10": "020000000c0002117e012fc80009040a00000000", - "11": "020000000d000a017e0107180009040b0000000100" + "4": "0000000b00000040190c011728000058000800000c0221fe00001c58", + "10": "", + "11": "00" }, "5": { - "6": "0000000000000000000000000000000000000000280000580000000000000000000000000000000000000000000000000000000000000000001000800300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000405132000206d336213160525064cbe7470cca8a7e79f3b470ea34af2ca04bc67049010e1e24a16800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000031c3d8b5c35010e1e15ab48000000000000000000000000000000b8f006ff10abbcff200011ff6f1f611f551e6052c50000f0ffff0900003a5c02325702d05b02c8d26600000000000000000000000000000000000035001e002203c30153015d0ea90e9b0166aee41703a0b5430000000000000000000000000000000000000000008b5c35010e1e15ab48000000000000000000000000000000b8f006ff10abbcff200011ff6f1f611f551e6052c50000f0ffff0900003a5c02325702d05b02c8d26600000000000000000000000000000000000035001e002203c30153015d0ea90e9b0166aee41703a0b543000000000000000000000000000000000000052a5800870f00870f010e1e00000000001900161d6fbcff200011ff6f1f611f551e605200000000000000000000000000000000000000000000000000000000000000000000005d007800f8028201fc01920bf10d6c03481a01000217144800000003ba31e41703a0b54300000000000000000000000000870f15012d014d017a01b701ff0103268c04a34954313936335331333737780100870f00870f010e1effff0000870f00870f020008c30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a349", - "12": "020000000c0009007e0102f80009050c00000000", - "24": "020000000c0003007e0101c80009051800000000" + "6": "280000580000000000000000000000000000000000000000000000000000000000000000001000800300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000405132000206d336213160525064cbe7470cca8a7e79f3b470ea34af2ca04bc67049010e1e24a16800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000031c3d8b5c35010e1e15ab48000000000000000000000000000000b8f006ff10abbcff200011ff6f1f611f551e6052c50000f0ffff0900003a5c02325702d05b02c8d26600000000000000000000000000000000000035001e002203c30153015d0ea90e9b0166aee41703a0b5430000000000000000000000000000000000000000008b5c35010e1e15ab48000000000000000000000000000000b8f006ff10abbcff200011ff6f1f611f551e6052c50000f0ffff0900003a5c02325702d05b02c8d26600000000000000000000000000000000000035001e002203c30153015d0ea90e9b0166aee41703a0b543000000000000000000000000000000000000052a5800870f00870f010e1e00000000001900161d6fbcff200011ff6f1f611f551e605200000000000000000000000000000000000000000000000000000000000000000000005d007800f8028201fc01920bf10d6c03481a01000217144800000003ba31e41703a0b54300000000000000000000000000870f15012d014d017a01b701ff0103268c04a34954313936335331333737780100870f00870f010e1effff0000870f00870f020008c30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a349", + "12": "", + "24": "" } }, - "1": "0200010010000b1a000000009e0000008000400000000158" + "1": "000000009e0000008000400000000158" } \ No newline at end of file diff --git a/src/server/control/gamepad.py b/src/server/control/gamepad.py index 5bd0cdd..c0e9c44 100644 --- a/src/server/control/gamepad.py +++ b/src/server/control/gamepad.py @@ -9,24 +9,33 @@ from src.server.net import socket_handlers from src.server.net import sockets from src.server.util.logging.logger_backend import LoggerBackend +from src.server.util.status_sending_thread import StatusSendingThread -class Gamepad: +class Gamepad(StatusSendingThread): NO_PACKETS = "NO_PACKETS" STOPPED = "STOPPED" RUNNING = "RUNNING" WAITING_FOR_PACKET = "WAITING_FOR_PACKET" + CRASHED = "CRASHED" def __init__(self): + """ + Backend server handler. Processes packets from the Wii U and servers clients. + """ + super().__init__() self.backend_thread = None - self.status = self.STOPPED - self.status_change_listeners = [] + self.set_status(self.STOPPED) self.running = False self.wii_packet_time = time.time() self.has_received_packet = False self.server = Server() def start(self): + """ + Start the main thread + :return: None + """ ConfigServer.load() ConfigServer.save() self.print_init() @@ -34,17 +43,26 @@ def start(self): socket_handlers.SocketHandlers.create() self.running = True LoggerBackend.debug("Starting backend thread") - self.backend_thread = Thread(target=self.update) + self.backend_thread = Thread(target=self.update, name="Backend Thread") self.backend_thread.start() LoggerBackend.debug("Post backend thread") def print_init(self): + """ + Log the initialization messages + :return: None + """ LoggerBackend.info("Started drc-sim-backend") self.print_config() LoggerBackend.info("Waiting for Wii U packets") @staticmethod def handle_wii_packet(sock): + """ + Receive data from a socket and pass it to the appropriate packet handler. + :param sock: Wii U datagram Socket + :return: None + """ data = sock.recv(2048) try: socket_handlers.SocketHandlers.wii_handlers[sock].update(data) @@ -52,6 +70,10 @@ def handle_wii_packet(sock): LoggerBackend.warn(str(e) + str(e.errno)) def handle_sockets(self): + """ + Check if any sockets have data and pass then to their handler. + :return: None + """ # Group all sockets rlist, wlist, xlist = select.select(socket_handlers.SocketHandlers.get_handler_keys(), (), (), 0.001) @@ -74,15 +96,24 @@ def handle_sockets(self): self.server.handle_client_command_packet(sock) def update(self): + """ + Main loop + :return: None + """ while self.running: try: self.check_last_packet_time() self.handle_sockets() Controller.update() except Exception as e: + self.set_status(self.CRASHED) LoggerBackend.throw(e) def close(self): + """ + Stop the backend thread + :return: None + """ if not self.running: LoggerBackend.debug("Ignored stop request: already stopped") return @@ -98,27 +129,29 @@ def close(self): s.close() LoggerBackend.debug("Closing sockets") sockets.Sockets.close() - self.status_change_listeners = [] + self.clear_status_change_listeners() LoggerBackend.debug("Backend closed") def check_last_packet_time(self): + """ + Checks if the server should shutdown after not receiving packets for more than a minute + :return: None + """ if not self.has_received_packet: status = self.WAITING_FOR_PACKET elif time.time() - self.wii_packet_time >= 60: status = Gamepad.NO_PACKETS else: status = Gamepad.RUNNING - if self.status != status: - self.status = status - for listener in self.status_change_listeners: - listener(status) + self.set_status(status) @staticmethod def print_config(): + """ + Logs the server configuration info + :return: None + """ LoggerBackend.info("Config: FPS %d", ConfigServer.fps) LoggerBackend.info("Config: Input Delay %f", ConfigServer.input_delay) LoggerBackend.info("Config: Image Quality %d", ConfigServer.quality) LoggerBackend.info("Config: Stream Audio %s", ConfigServer.stream_audio) - - def add_status_change_listener(self, callback): - self.status_change_listeners.append(callback) diff --git a/src/server/control/util/controller.py b/src/server/control/util/controller.py index d59e3c2..41d4a58 100644 --- a/src/server/control/util/controller.py +++ b/src/server/control/util/controller.py @@ -1,8 +1,10 @@ -import array import time +import array + from src.server.data import constants from src.server.data.config_server import ConfigServer +from src.server.data.struct import input from src.server.net import sockets from src.server.net.codec import Codec @@ -13,65 +15,20 @@ class Controller: HID_UPDATE_INTERVAL = int((1. / 180.) * 1000.) # 5 - leaving it since it may make sense later # Button buffers button_buffer = (0, 0) - l3r3_buffer = (0, 0) + extra_button_buffer = (0, 0) joystick_buffer = ((0, 0, 0, 0), 0) touch_buffer = (((-1, -1), (-1, -1)), 0) + send_audio = (False, 0) def __init__(self): pass - # The following get_####_input_report methods modify a passed report array - - @classmethod - def get_button_input_report(cls, report): - button_bits = cls.get_button_input() - # 16bit @ 2 - report[1] = (button_bits >> 8) | ((button_bits & 0xff) << 8) - return report - - @classmethod - def get_l3_r3_input_report(cls, report): - # 8bit @ 80 - l3r3_bits = cls.get_l3_r3_input() - report[40] = l3r3_bits - return report - @classmethod def scale_stick(cls, old_value, old_min, old_max, new_min, new_max): return int((((old_value - old_min) * (new_max - new_min)) / (old_max - old_min)) + new_min) - @classmethod - def get_joystick_input_report(cls, report): - # 16bit LE array @ 6 - # LX, LY, RX, RY - # 0: l stick l/r - # 1: l stick u/d - # 2: l trigger - # 3: r stick l/r - # 4: r stick u/d - # 5: r trigger - for axis in range(4): - orig = cls.get_joystick_input(axis) - scaled = 0x800 - if abs(orig) > 0.2: - if axis in (0, 3): - scaled = cls.scale_stick(orig, -1, 1, 900, 3200) - elif axis in (1, 4): - scaled = cls.scale_stick(orig, 1, -1, 900, 3200) - # print '%04i %04i %f' % (i, scaled, orig) - report[3 + axis] = scaled - return report - @classmethod def get_touch_input_report(cls, report): - # touchpanel crap @ 36 - 76 - # byte_18 = 0 - byte_17 = 3 - # byte_9b8 = 0 - byte_9fd = 6 - umi_fw_rev = 0x40 - # byte_9fb = 0 - byte_19 = 2 point, screen = cls.get_touch_input() if point[0] >= 0 and point[1] >= 0: x = cls.scale_stick(point[0], 0, screen[0], 200, 3800) @@ -86,19 +43,6 @@ def get_touch_input_report(cls, report): report[18 + 0 * 2 + 1] |= ((z1 >> 3) & 7) << 12 report[18 + 1 * 2 + 0] |= ((z1 >> 6) & 7) << 12 report[18 + 1 * 2 + 1] |= ((z1 >> 9) & 7) << 12 - - report[18 + 3 * 2 + 1] |= ((byte_17 >> 0) & 7) << 12 - report[18 + 4 * 2 + 0] |= ((byte_17 >> 3) & 7) << 12 - report[18 + 4 * 2 + 1] |= ((byte_17 >> 6) & 3) << 12 - - report[18 + 5 * 2 + 0] |= ((byte_9fd >> 0) & 7) << 12 - report[18 + 5 * 2 + 1] |= ((byte_9fd >> 3) & 7) << 12 - report[18 + 6 * 2 + 0] |= ((byte_9fd >> 6) & 3) << 12 - - report[18 + 7 * 2 + 0] |= ((umi_fw_rev >> 4) & 7) << 12 - - # TODO checkout what's up with | 4 - report[18 + 9 * 2 + 1] |= ((byte_19 & 2) | 4) << 12 return report # Getters @@ -116,10 +60,10 @@ def get_button_input(cls): return cls.button_buffer[0] @classmethod - def get_l3_r3_input(cls): - if not cls.is_input_within_timeframe(cls.l3r3_buffer): + def get_extra_button_input(cls): + if not cls.is_input_within_timeframe(cls.extra_button_buffer): return 0 - return cls.l3r3_buffer[0] + return cls.extra_button_buffer[0] @classmethod def get_joystick_input(cls, joystick_id): @@ -133,13 +77,19 @@ def get_touch_input(cls): return (-1, -1), (-1, -1) return cls.touch_buffer[0] + @classmethod + def get_send_audio(cls): + if not cls.is_input_within_timeframe(cls.send_audio): + return False + return cls.send_audio + @classmethod def set_button_input(cls, data): cls.button_buffer = Codec.decode_input(data) @classmethod - def set_l3r3_input(cls, data): - cls.l3r3_buffer = Codec.decode_input(data) + def set_extra_button_input(cls, data): + cls.extra_button_buffer = Codec.decode_input(data) @classmethod def set_touch_input(cls, data): @@ -149,26 +99,35 @@ def set_touch_input(cls, data): def set_joystick_input(cls, data): cls.joystick_buffer = Codec.decode_input(data) - # Update + @classmethod + def set_send_audio(cls, data): + cls.send_audio = Codec.decode_input(data) @classmethod def send_hid_update(cls): - - report = array.array('H', [0] * 128) - - # 16bit LE @ 0 seq_id - # seems to be ignored - report[0] = cls.hid_seq_id - - report = cls.get_button_input_report(report) - report = cls.get_l3_r3_input_report(report) - report = cls.get_joystick_input_report(report) - report = cls.get_touch_input_report(report) - - # 16bit @ 126 - report[0x3f] = 0xe000 - # print report.tostring().encode('hex') - sockets.Sockets.WII_HID_S.sendto(report, ('192.168.1.10', constants.PORT_WII_HID)) + report_array = array.array("H", b"\x00" * input.input_data.sizeof()) + report_array = cls.get_touch_input_report(report_array) # TODO handle this in the struct + report = input.input_data.parse(report_array.tobytes()) + + report.sequence_id = cls.hid_seq_id + report.buttons = cls.get_button_input() + report.power_status = 0 + report.battery_charge = 0 + report.extra_buttons = cls.get_extra_button_input() + report.left_stick_x = 8 + int(cls.get_joystick_input(0) * 8) + report.left_stick_y = 8 - int(cls.get_joystick_input(1) * 8) + report.right_stick_x = 8 + int(cls.get_joystick_input(2) * 8) + report.right_stick_y = 8 - int(cls.get_joystick_input(3) * 8) + report.audio_volume = 0 + report.accel_x = 0 + report.accel_y = 0 + report.accel_z = 0 + report.gyro_roll = 0 + report.gyro_yaw = 0 + report.gyro_pitch = 0 + report.fw_version_neg = 215 + + sockets.Sockets.WII_HID_S.sendto(input.input_data.build(report), ('192.168.1.10', constants.PORT_WII_HID)) cls.hid_seq_id = (cls.hid_seq_id + 1) % 65535 @classmethod diff --git a/src/server/data/constants.py b/src/server/data/constants.py index bd69333..b0ebd60 100644 --- a/src/server/data/constants.py +++ b/src/server/data/constants.py @@ -1,7 +1,7 @@ import os # Info -VERSION = "1.4" +VERSION = "1.5" # Port PORT_WII_MSG = 50010 @@ -28,6 +28,7 @@ COMMAND_VIBRATE = b"VIBRATE" COMMAND_PING = b"PING" COMMAND_PONG = b"PONG" +COMMAND_INPUT_MIC_BLOW = b"INPUT_MIC_BLOW" # Paths PATH_ROOT = os.path.expanduser("~/.drc-sim/") diff --git a/src/server/data/struct/input.py b/src/server/data/struct/input.py index 1175898..4919067 100644 --- a/src/server/data/struct/input.py +++ b/src/server/data/struct/input.py @@ -7,9 +7,9 @@ ) gyroscope_data = construct.BitStruct( - "gyro_roll" / construct.Int24sl, - "gyro_yaw" / construct.Int24sl, - "gyro_pitch" / construct.Int24sl + "gyro_roll" / construct.BitsInteger(24), + "gyro_yaw" / construct.BitsInteger(24), + "gyro_pitch" / construct.BitsInteger(24) ) magnet_data = construct.Struct( @@ -22,10 +22,10 @@ "touch_value" / construct.BitsInteger(12) ) touchscreen_points_data = construct.Struct( - construct.Array(2, touchscreen_coords_data) + "coords" / construct.Array(2, touchscreen_coords_data) ) touchscreen_data = construct.Struct( - construct.Array(10, touchscreen_points_data) + "points" / construct.Array(10, touchscreen_points_data) ) input_data = construct.Struct( diff --git a/src/server/net/server/command.py b/src/server/net/server/command.py index 1732701..d87b504 100644 --- a/src/server/net/server/command.py +++ b/src/server/net/server/command.py @@ -1,7 +1,5 @@ -import codecs - -from src.server.data import constants from src.server.control.util.controller import Controller +from src.server.data import constants from src.server.net import sockets from src.server.net.codec import Codec from src.server.util.logging.logger_backend import LoggerBackend @@ -21,20 +19,22 @@ def update(self, address, command, data): elif command == constants.COMMAND_INPUT_BUTTON: Controller.set_button_input(data) elif command == constants.COMMAND_INPUT_L3R3: - Controller.set_l3r3_input(data) + Controller.set_extra_button_input(data) elif command == constants.COMMAND_INPUT_TOUCH: Controller.set_touch_input(data) elif command == constants.COMMAND_INPUT_JOYSTICK: Controller.set_joystick_input(data) elif command == constants.COMMAND_PING: sockets.Sockets.SERVER_CMD_S.sendto(Codec.encode_command(constants.COMMAND_PONG), address) + elif command == constants.COMMAND_INPUT_MIC_BLOW: + Controller.set_send_audio(data) @staticmethod def register_client(address): sockets.Sockets.add_client_socket(address, ServiceCMD) @classmethod - def broadcast(cls, command, data=""): + def broadcast(cls, command, data=b""): sockets.Sockets.broadcast_command_packet(command, data, ServiceCMD.__name__) diff --git a/src/server/net/wii/audio.py b/src/server/net/wii/audio.py index 13fd606..eab1e69 100644 --- a/src/server/net/wii/audio.py +++ b/src/server/net/wii/audio.py @@ -1,6 +1,11 @@ +import codecs +import random + +from src.server.control.util.controller import Controller from src.server.data import constants from src.server.data.config_server import ConfigServer from src.server.data.struct import audio +from src.server.net import sockets from src.server.net.server.audio import ServiceAUD from src.server.net.server.command import ServiceCMD from src.server.net.wii.base import ServiceBase @@ -10,6 +15,14 @@ class AudioHandler(ServiceBase): def __init__(self): super(AudioHandler, self).__init__() + self.random_audio = "" + for byte in range(0, 512): + random_byte = hex(random.randint(0, 255)).replace("0x", "", 1) + if len(random_byte) == 1: + self.random_audio += "0" + self.random_audio += random_byte + LoggerBackend.debug("Random audio (%d bytes)", len(self.random_audio) / 2) + LoggerBackend.extra("Random audio: %s", self.random_audio) def close(self): pass @@ -33,3 +46,19 @@ def update(self, packet): if ConfigServer.stream_audio: ServiceAUD.broadcast(packet[8:]) + + if Controller.get_send_audio(): + self.send_audio(h.seq_id) + + def send_audio(self, sid): + header = audio.header.build(dict( + fmt=6, + channel=1, + vibrate=False, + packet_type=0, + seq_id=sid, + payload_size=512, + timestamp=0 + )) + data = codecs.decode(self.random_audio, "hex") + sockets.Sockets.WII_AUD_S.sendto(header + data, ('192.168.1.10', constants.PORT_WII_AUD)) diff --git a/src/server/net/wii/command.py b/src/server/net/wii/command.py index bc6d83e..874f280 100644 --- a/src/server/net/wii/command.py +++ b/src/server/net/wii/command.py @@ -51,12 +51,12 @@ def cmd0(self, h): LoggerBackend.debug('unhandled CMD0 %s %s', id_primary, id_secondary) return response = self.command_responses["0"][id_primary][id_secondary] - response = codecs.decode(response[40:], "hex") + response = codecs.decode(response, "hex") self.send_response_cmd0(h, response) def cmd1(self, h): response = self.command_responses["1"] - response = codecs.decode(response[16:], "hex") + response = codecs.decode(response, "hex") self.send_response(h, response) def cmd2(self, h): diff --git a/src/server/ui/gui/frame/frame_run_server.py b/src/server/ui/gui/frame/frame_run_server.py index 5a7a5f8..ab0851a 100644 --- a/src/server/ui/gui/frame/frame_run_server.py +++ b/src/server/ui/gui/frame/frame_run_server.py @@ -14,6 +14,11 @@ class FrameRunServer(FrameTab): def __init__(self, master=None, **kw): + """ + GUI tab that handles interface and region selection and starting the server. + :param master: root window + :param kw: args + """ FrameTab.__init__(self, master, **kw) self.wii_u_interface = None self.normal_interface = None @@ -54,6 +59,11 @@ def __init__(self, master=None, **kw): LoggerGui.extra("Initialized FrameRunServer") def start_server(self, event=None): + """ + Try to start wpa_supplicant and connect to a Wii U. + :param event: Determines if this was a user initiated start. + :return: None + """ if event: LoggerGui.debug("User clicked start server button") LoggerGui.debug("Start server called") @@ -101,6 +111,11 @@ def start_server(self, event=None): self.label_backend_status.config(text="WAITING") def wpa_status_changed(self, status): + """ + Handles wpa status changes. Initializes backend server if a connection is made. + :param status: status message + :return: None + """ LoggerGui.debug("Wpa changed status to %s", status) self.label_wpa_status.config(text=status) if status == WpaSupplicant.CONNECTED: @@ -128,12 +143,22 @@ def wpa_status_changed(self, status): "Check %s for details." % constants.PATH_LOG_WPA) def backend_status_changed(self, status): + """ + Handles backend status changes. + :param status: status message + :return: None + """ LoggerGui.debug("Backend status changed to %s", status) self.label_backend_status.config(text=status) - if status == Gamepad.NO_PACKETS: + if status in (Gamepad.NO_PACKETS, Gamepad.CRASHED): self.stop_server() def stop_server(self, event=None): + """ + Stops active threads. + :param event: Determines if this is a user initiated stop + :return: None + """ if event: LoggerGui.debug("User clicked stop server button") LoggerGui.debug("Stop server called") @@ -148,6 +173,10 @@ def stop_server(self, event=None): self.activate() def activate(self): + """ + Initializes the frame. + :return: None + """ LoggerGui.debug("FrameRunServer activated") self.dropdown_wiiu_interface["values"] = InterfaceUtil.get_wiiu_compatible_interfaces() self.dropdown_normal_interface["values"] = InterfaceUtil.get_all_interfaces() @@ -159,5 +188,9 @@ def activate(self): self.label_interface_info.config(text="") def deactivate(self): + """ + De-initializes the frame. + :return: None + """ LoggerGui.debug("FrameRunServer deactivated") self.stop_server() diff --git a/src/server/ui/gui/gui_main.py b/src/server/ui/gui/gui_main.py index fc43500..30bfe21 100644 --- a/src/server/ui/gui/gui_main.py +++ b/src/server/ui/gui/gui_main.py @@ -9,6 +9,9 @@ class GuiMain: def __init__(self): + """ + Main Gui Entrance + """ tkinter.Tk.report_callback_exception = self.throw # Main window self.destroyed = False @@ -32,23 +35,44 @@ def __init__(self): @staticmethod def throw(*args): + """ + Throw exceptions from Tkinter + :param args: arguments + :return: None + """ for arg in args: if isinstance(arg, Exception): LoggerGui.throw(arg) def after(self): + """ + Empty loop to catch KeyboardInterrupt + :return: None + """ self.main_window.after(1000, self.after) def start(self): + """ + Start the main window loop + :return: + """ LoggerGui.info("Opening GUI") self.after() self.main_window.mainloop() LoggerGui.info("GUI Closed") def stop(self): + """ + Convenience function to call on_closing() + :return: None + """ self.on_closing() def on_closing(self): + """ + Close the main window and current tab + :return: None + """ if self.destroyed: return self.destroyed = True @@ -60,7 +84,13 @@ def on_closing(self): except Exception as e: LoggerGui.exception(e) + # noinspection PyUnusedLocal def on_tab_changed(self, event): + """ + Close the previous tab and initialize a new one + :param event: tab event + :return: None + """ tab_id = self.notebook.select() tab_index = self.notebook.index(tab_id) tab_name = self.notebook.tab(tab_index, "text") diff --git a/src/server/util/status_sending_thread.py b/src/server/util/status_sending_thread.py new file mode 100644 index 0000000..c885bf4 --- /dev/null +++ b/src/server/util/status_sending_thread.py @@ -0,0 +1,33 @@ +class StatusSendingThread: + def __init__(self): + """ + Helper for registering callback status. + """ + self.status_change_listeners = [] + self.status = None + + def set_status(self, status): + """ + Set and notify callbacks if the status does not match the current status. + :param status: status message + :return: None + """ + if self.status != status: + self.status = status + for listener in self.status_change_listeners: + listener(status) + + def add_status_change_listener(self, callback): + """ + Add a callback that will be called on status change. + :param callback: callable function + :return: None + """ + self.status_change_listeners.append(callback) + + def clear_status_change_listeners(self): + """ + Cleat the status callbacks. + :return: None + """ + self.status_change_listeners = [] diff --git a/src/server/util/wpa_supplicant.py b/src/server/util/wpa_supplicant.py index 8662f07..374c575 100644 --- a/src/server/util/wpa_supplicant.py +++ b/src/server/util/wpa_supplicant.py @@ -9,9 +9,10 @@ from src.server.data.config_server import ConfigServer from src.server.util.logging.logger_wpa import LoggerWpa from src.server.util.process_util import ProcessUtil +from src.server.util.status_sending_thread import StatusSendingThread -class WpaSupplicant: +class WpaSupplicant(StatusSendingThread): UNKNOWN = "UNKNOWN" CONNECTING = "CONNECTING" CONNECTED = "CONNECTED" @@ -22,6 +23,10 @@ class WpaSupplicant: FAILED_START = "FAILED_START" def __init__(self): + """ + Helper for interacting with wpa_supplicant_drc and wpa_cli_drc. + """ + super().__init__() self.time_scan = 0 self.time_start = 0 self.mac_addr_regex = re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') @@ -37,6 +42,18 @@ def __init__(self): self.psk_thread_cli = None def connect(self, conf_path, interface, status_check=True): + """ + Starts a thread that connects to the Wii U. + Status: + FAILED_START: wpa_supplicant_drc did not initialize + SCANNING: wpa_supplicant_drc is scanning + CONNECTED: wpa_supplicant_drc is connected to an AP + CONNECTING: wpa_supplicant_drc is authenticating + TERMINATED: wpa_supplicant_drc was found by the T-1000 Cyberdyne Systems Model 101 + NOT_FOUND: wpa_supplicant_drc could not find a Wii U AP + UNKNOWN: wpa_supplicant_drc is in a state that is unhandled - it will be logged + :return: None + """ LoggerWpa.debug("Connect called") self.running = True self.unblock_wlan() @@ -52,10 +69,15 @@ def connect(self, conf_path, interface, status_check=True): LoggerWpa.debug("Started wpa supplicant") if status_check: LoggerWpa.debug("Starting status check thread") - self.status_check_thread = Thread(target=self.check_status) + self.status_check_thread = Thread(target=self.check_status, name="WPA Status Check Thread") self.status_check_thread.start() def check_status(self): + """ + Thread that checks WPA status every second + Updates + :return: None + """ while self.running: wpa_status = self.wpa_cli("status") scan_results = self.wpa_cli("scan_results") @@ -90,27 +112,41 @@ def check_status(self): else: LoggerWpa.extra("WPA status: %s", wpa_status) status = self.UNKNOWN - if status != self.status: - self.status = status - for callback in self.status_changed_listeners: - callback(self.status) + self.set_status(status) time.sleep(1) @staticmethod def kill_wpa(): + """ + Makes a system call to kill wpa_supplicant_drc + :return: None + """ ProcessUtil.call(["killall", "wpa_supplicant_drc"]) @staticmethod def unblock_wlan(): + """ + Make a system call to unblock wlan + :return: None + """ ProcessUtil.call(["rfkill", "unblock", "wlan"]) @staticmethod def wpa_cli(command): + """ + Makes a system call to wpa_cli_drc + :param command: command to pass to wpa_cli_drc + :return: command output + """ if isinstance(command, str): command = [command] return ProcessUtil.get_output(["wpa_cli_drc", "-p", "/var/run/wpa_supplicant_drc"] + command, silent=True) def stop(self): + """ + Stops any background thread that is running + :return: None + """ if not self.running: LoggerWpa.debug("Ignored stop request: already stopped") return @@ -130,50 +166,53 @@ def stop(self): self.wpa_supplicant_process.terminate() self.kill_wpa() # reset - self.status_changed_listeners = [] + self.clear_status_change_listeners() self.time_start = 0 self.time_scan = 0 LoggerWpa.debug("Wpa stopped") - def add_status_change_listener(self, callback): + def scan_contains_wii_u(self, scan_results): """ - Calls passed method when status changed. - If listening to a "connect" call: - FAILED_START: wpa_supplicant_drc did not initialize - SCANNING: wpa_supplicant_drc is scanning - CONNECTED: wpa_supplicant_drc is connected to an AP - CONNECTING: wpa_supplicant_drc is authenticating - TERMINATED: wpa_supplicant_drc was found by the T-1000 Cyberdyne Systems Model 101 - NOT_FOUND: wpa_supplicant_drc could not find a Wii U AP - UNKNOWN: wpa_supplicant_drc is in a state that is unhandled - it will be logged - If listening to a "get_psk" call: - FAILED_START: there was an error attempting to parse CLI output - exception is logged - NOT_FOUND: wpa_supplicant_drc did not find any Wii U APs - TERMINATED: wpa_supplicant_drc could not authenticate with any SSIDs - DISCONNECTED: auth details were saved - :param callback: method to call on status change - :return: None + Check if string contains Wii U SSID + :param scan_results: string + :return: boolean """ - self.status_changed_listeners.append(callback) - - def scan_contains_wii_u(self, scan_results): - for line in scan_results.split("\n"): + for line in scan_results.splitlines(): if self.wiiu_ap_regex.match(line): return True return False def scan_is_empty(self, scan_results): + """ + Check if the scan has no MAC addresses + :param scan_results: string + :return: boolean + """ for line in scan_results.split("\n"): if self.mac_addr_regex.match(line): return False return True def get_psk(self, conf_path, interface, code): + """ + Starts a thread to connect and attempt to obtain a Wii U's PSK + Status: + FAILED_START: there was an error attempting to parse CLI output - exception is logged + NOT_FOUND: wpa_supplicant_drc did not find any Wii U APs + TERMINATED: wpa_supplicant_drc could not authenticate with any SSIDs + DISCONNECTED: auth details were saved + :return: None + """ self.connect(conf_path, interface, status_check=False) self.psk_thread = Thread(target=self.get_psk_thread, kwargs={"code": code}, name="PSK Thread") self.psk_thread.start() def get_psk_thread(self, code): + """ + Thread that attempts to authenticate with a Wii U. Updates status + :param code: WPS PIN + :return: None + """ try: LoggerWpa.debug("CLI expect starting") self.psk_thread_cli = pexpect.spawn("wpa_cli_drc -p /var/run/wpa_supplicant_drc") @@ -240,6 +279,12 @@ def get_psk_thread(self, code): @staticmethod def save_connect_conf(bssid): + """ + Modify the temp get_psk configuration to be a connect configurraton and save it to + ~/.drc-sim/connect_to_wii_u.conf. + :param bssid: Wii U BSSID + :return: None + """ LoggerWpa.debug("Saving connection config") # add additional connect information to config conf = open(constants.PATH_CONF_CONNECT_TMP, "r")