From 7aa7099c5fbde76980774422ede93a0de415ec0c Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 22 Jun 2019 12:06:00 -0700 Subject: [PATCH 1/9] Throttle stack frames on playback with respect to time --- bridge.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/bridge.py b/bridge.py index 27defab..e867911 100755 --- a/bridge.py +++ b/bridge.py @@ -10,9 +10,12 @@ import binascii import serial import math -import time +import datetime as dt +import pickle from tqdm import tqdm +from collections import namedtuple + def enumerate_controllers(): print('Controllers connected to this system:') @@ -87,23 +90,29 @@ def controller_states(controller_id): except AttributeError: print('Using controller {:s} for input.'.format(controller_id)) + start_dttm = dt.datetime.now().timestamp() + while True: - buttons = sum([sdl2.SDL_GameControllerGetButton(controller, b)< trigger_deadzone) << 6 - buttons |= (abs(sdl2.SDL_GameControllerGetAxis(controller, sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > trigger_deadzone) << 7 + elaped_time = dt.datetime.now().timestamp() - start_dttm + buttons = sum([sdl2.SDL_GameControllerGetButton(controller, b) << n for n, b in enumerate(buttonmapping)]) + buttons |= (abs(sdl2.SDL_GameControllerGetAxis(controller, sdl2.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > trigger_deadzone) << 6 + buttons |= (abs(sdl2.SDL_GameControllerGetAxis(controller, sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > trigger_deadzone) << 7 - hat = hatcodes[sum([sdl2.SDL_GameControllerGetButton(controller, b)<> 8) + 128 for x in rawaxis] rawbytes = struct.pack('>BHBBBB', hat, buttons, *axis) - yield binascii.hexlify(rawbytes) + b'\n' + message = binascii.hexlify(rawbytes) + b'\n' + message_stamp = ControllerStateTime(message, elaped_time) + yield message_stamp def replay_states(filename): with open(filename, 'rb') as replay: - yield from replay.readlines() + for line in replay.readlines(): + yield pickle.loads(line) def example_macro(): @@ -118,8 +127,6 @@ def example_macro(): yield binascii.hexlify(rawbytes) + b'\n' - - class InputStack(object): def __init__(self): self.l = [] @@ -143,6 +150,7 @@ def __next__(self): raise StopIteration +ControllerStateTime = namedtuple('ControllerStateTime', ('message', 'delta')) if __name__ == '__main__': @@ -171,7 +179,7 @@ def __next__(self): if args.playback is None or args.dontexit: live = controller_states(args.controller) - next(live) # pull a controller update to make it print the name before starting speed meter + next(live) # pull a controller update to make it print the name before starting speed meter input_stack.push(live) if args.playback is not None: input_stack.push(replay_states(args.playback)) @@ -179,8 +187,8 @@ def __next__(self): with (open(args.record, 'wb') if args.record is not None else contextmanager(lambda: iter([None]))()) as record: with tqdm(unit=' updates', disable=args.quiet) as pbar: try: + start_dttm = dt.datetime.now().timestamp() while True: - for event in sdl2.ext.get_events(): # we have to fetch the events from SDL in order for the controller # state to be updated. @@ -191,19 +199,22 @@ def __next__(self): # input_stack.push(example_macro()) # or play from file: # input_stack.push(replay_states(filename)) - - pass + break try: - message = next(input_stack) - ser.write(message) + msg_stamp = next(input_stack) + while True: + elapsed_delta = dt.datetime.now().timestamp() - start_dttm + if msg_stamp.delta < elapsed_delta: + break + ser.write(msg_stamp.message) if record is not None: - record.write(message) + record.write(pickle.dumps(msg_stamp)) except StopIteration: break # update speed meter on console. - pbar.set_description('Sent {:s}'.format(message[:-1].decode('utf8'))) + pbar.set_description('Sent {:s}'.format(msg_stamp.message[:-1].decode('utf8'))) pbar.update() while True: From 1ca9c7ef238d673aba4175f5ed4a9a294341878a Mon Sep 17 00:00:00 2001 From: ahmed Date: Sat, 22 Jun 2019 13:52:05 -0700 Subject: [PATCH 2/9] Fix serialize and deserialization issues --- bridge.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/bridge.py b/bridge.py index e867911..0503eba 100755 --- a/bridge.py +++ b/bridge.py @@ -77,6 +77,8 @@ def get_controller(c): axis_deadzone = 1000 trigger_deadzone = 0 +start_dttm = dt.datetime.now().timestamp() + def controller_states(controller_id): @@ -90,8 +92,6 @@ def controller_states(controller_id): except AttributeError: print('Using controller {:s} for input.'.format(controller_id)) - start_dttm = dt.datetime.now().timestamp() - while True: elaped_time = dt.datetime.now().timestamp() - start_dttm buttons = sum([sdl2.SDL_GameControllerGetButton(controller, b) << n for n, b in enumerate(buttonmapping)]) @@ -104,15 +104,15 @@ def controller_states(controller_id): axis = [((0 if abs(x) < axis_deadzone else x) >> 8) + 128 for x in rawaxis] rawbytes = struct.pack('>BHBBBB', hat, buttons, *axis) - message = binascii.hexlify(rawbytes) + b'\n' - message_stamp = ControllerStateTime(message, elaped_time) + message_stamp = ControllerStateTime(rawbytes, elaped_time) yield message_stamp def replay_states(filename): with open(filename, 'rb') as replay: for line in replay.readlines(): - yield pickle.loads(line) + # remove new-line character at end of line, and feed it into deserializer + yield ControllerStateTime.deserialize(line[:-1]) def example_macro(): @@ -124,7 +124,7 @@ def example_macro(): lx = int((1.0 + math.sin(2 * math.pi * i / 240)) * 127) ly = int((1.0 + math.cos(2 * math.pi * i / 240)) * 127) rawbytes = struct.pack('>BHBBBB', hat, buttons, lx, ly, rx, ry) - yield binascii.hexlify(rawbytes) + b'\n' + yield rawbytes class InputStack(object): @@ -150,7 +150,17 @@ def __next__(self): raise StopIteration -ControllerStateTime = namedtuple('ControllerStateTime', ('message', 'delta')) +class ControllerStateTime (namedtuple('ControllerStateTime', ('message', 'delta'))): + def formatted_message(self): + return binascii.hexlify(self.message) + b'\n' + + def serialize(self): + return binascii.hexlify(pickle.dumps(self)) + + @staticmethod + def deserialize(self): + return pickle.loads(binascii.unhexlify(self)) + if __name__ == '__main__': @@ -187,7 +197,6 @@ def __next__(self): with (open(args.record, 'wb') if args.record is not None else contextmanager(lambda: iter([None]))()) as record: with tqdm(unit=' updates', disable=args.quiet) as pbar: try: - start_dttm = dt.datetime.now().timestamp() while True: for event in sdl2.ext.get_events(): # we have to fetch the events from SDL in order for the controller @@ -199,7 +208,7 @@ def __next__(self): # input_stack.push(example_macro()) # or play from file: # input_stack.push(replay_states(filename)) - break + pass try: msg_stamp = next(input_stack) @@ -207,14 +216,14 @@ def __next__(self): elapsed_delta = dt.datetime.now().timestamp() - start_dttm if msg_stamp.delta < elapsed_delta: break - ser.write(msg_stamp.message) + ser.write(msg_stamp.formatted_message()) if record is not None: - record.write(pickle.dumps(msg_stamp)) + record.write(msg_stamp.serialize() + b'\n') except StopIteration: break # update speed meter on console. - pbar.set_description('Sent {:s}'.format(msg_stamp.message[:-1].decode('utf8'))) + pbar.set_description('Sent {:s}'.format(msg_stamp.formatted_message()[:-1].decode('utf8'))) pbar.update() while True: From ca05c47c9cd9aad4abc13e1cb5a28c7603b95c60 Mon Sep 17 00:00:00 2001 From: ahmed Date: Thu, 27 Jun 2019 19:10:27 -0700 Subject: [PATCH 3/9] Try not to spam the stack with needless calls --- bridge.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/bridge.py b/bridge.py index 0503eba..5600446 100755 --- a/bridge.py +++ b/bridge.py @@ -121,10 +121,12 @@ def example_macro(): rx = 128 ry = 128 for i in range(240): + elapsed_time = dt.datetime.now().timestamp() - start_dttm lx = int((1.0 + math.sin(2 * math.pi * i / 240)) * 127) ly = int((1.0 + math.cos(2 * math.pi * i / 240)) * 127) rawbytes = struct.pack('>BHBBBB', hat, buttons, lx, ly, rx, ry) - yield rawbytes + message_stamp = ControllerStateTime(rawbytes, elapsed_time) + yield message_stamp class InputStack(object): @@ -151,6 +153,9 @@ def __next__(self): class ControllerStateTime (namedtuple('ControllerStateTime', ('message', 'delta'))): + """ + Serializable object responsible for recording a particular input at a particular timestamp + """ def formatted_message(self): return binascii.hexlify(self.message) + b'\n' @@ -197,26 +202,32 @@ def deserialize(self): with (open(args.record, 'wb') if args.record is not None else contextmanager(lambda: iter([None]))()) as record: with tqdm(unit=' updates', disable=args.quiet) as pbar: try: + prev_msg_stamp = None while True: for event in sdl2.ext.get_events(): # we have to fetch the events from SDL in order for the controller # state to be updated. # example of running a macro when a joystick button is pressed: - #if event.type == sdl2.SDL_JOYBUTTONDOWN: - # if event.jbutton.button == 1: - # input_stack.push(example_macro()) + # if event.type == sdl2.SDL_CONTROLLERBUTTONDOWN: + # # if event.jbutton.button == 1: + # input_stack.push(example_macro()) # or play from file: # input_stack.push(replay_states(filename)) pass try: msg_stamp = next(input_stack) + # This this input has aleady been entered, then don't spam the stack + if prev_msg_stamp and msg_stamp.message == prev_msg_stamp.message: + continue + # Wait for the correct amount of time to pass before performing an input while True: elapsed_delta = dt.datetime.now().timestamp() - start_dttm if msg_stamp.delta < elapsed_delta: break ser.write(msg_stamp.formatted_message()) + prev_msg_stamp = msg_stamp if record is not None: record.write(msg_stamp.serialize() + b'\n') except StopIteration: From dbbb6abbbadb928233616b61806313a547a78ec8 Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 29 Jun 2019 12:36:09 -0700 Subject: [PATCH 4/9] Add Websocket support to allow for remote connections on different machines --- controller-client.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ controller-remote.py | 48 +++++++++++++++++++++++++++++++++ controller-server.py | 40 +++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 controller-client.py create mode 100644 controller-remote.py create mode 100644 controller-server.py diff --git a/controller-client.py b/controller-client.py new file mode 100644 index 0000000..ab74ec2 --- /dev/null +++ b/controller-client.py @@ -0,0 +1,64 @@ +import socketio +from socketio.exceptions import ConnectionError +import bridge +import serial +import argparse + +# standard Python +sio = socketio.Client() +connected = False +ser = None + + +@sio.on('controller-input') +def on_message(data): + while True: + # wait for the arduino to request another state. + response = ser.read(1) + if response == b'U': + break + elif response == b'X': + print('Arduino reported buffer overrun.') + message_stamp = bridge.ControllerStateTime(data, 0) + print(data) + ser.write(message_stamp.formatted_message()) + + +@sio.event +def connect(): + global connected + connected = True + print("I'm connected!") + + +@sio.event +def disconnect(): + global connected + connected = False + print("I'm disconnected!") + import time + num_tries = 0 + while num_tries < 5: + try: + print('attempting to reconnect...') + sio.connect('http://localhost:5000') + except ConnectionError: + print('failed...') + time.sleep(5) + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + parser.add_argument('-h', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') + parser.add_argument('-c', '--controller', type=str, default='0', help='Controller to use. Default: 0.') + parser.add_argument('-b', '--baud-rate', type=int, default=115200, help='Baud rate. Default: 115200.') + parser.add_argument('-p', '--port', type=str, default='/dev/ttyUSB0', help='Serial port. Default: /dev/ttyUSB0.') + + args = parser.parse_args() + + sio.connect(args.host) + ser = serial.Serial(args.port, args.baud_rate, bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=None) + print('Using {:s} at {:d} baud for comms.'.format(args.port, args.baud_rate)) + diff --git a/controller-remote.py b/controller-remote.py new file mode 100644 index 0000000..47a87c3 --- /dev/null +++ b/controller-remote.py @@ -0,0 +1,48 @@ +import socketio +from socketio.exceptions import ConnectionError +import bridge +import sdl2 + +# standard Python +sio = socketio.Client() +connected = False + + +@sio.event +def connect(): + global connected + connected = True + print("I'm connected!") + + +@sio.event +def disconnect(): + global connected + connected = False + print("I'm disconnected!") + import time + num_tries = 0 + while num_tries < 5: + try: + print('attempting to reconnect...') + sio.connect('http://localhost:5000') + except ConnectionError: + print('failed...') + time.sleep(5) + + +def init_input_loop(joystick_idx): + inputs = bridge.controller_states(joystick_idx) + prev_message = None + while connected: + sdl2.ext.get_events() + message_stamp = next(inputs) + message = message_stamp.formatted_message() + if message != prev_message: + sio.emit('controller-input', message) + prev_message = message + + +if __name__ == '__main__': + sio.connect('http://localhost:5000') + init_input_loop('0') diff --git a/controller-server.py b/controller-server.py new file mode 100644 index 0000000..8a399c2 --- /dev/null +++ b/controller-server.py @@ -0,0 +1,40 @@ +""" +Purpose: Create a flask websocket server that can take in a controller input & relay it to my PC which relays it to my +switch. Basically, I got tired of having to be home to work on this project. +""" +import os +from flask import Flask, jsonify +from flask_socketio import SocketIO, emit + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET', 'secret!') +socketio = SocketIO(app) +client_count = 0 + + +@app.route('/') +def index(): + return jsonify({ + 'client_count': client_count + }) + + +@socketio.on('controller-input') +def test_message(message): + emit('controller-input', message, broadcast=True) + + +@socketio.on('connect') +def test_connect(): + global client_count + client_count += 1 + + +@socketio.on('disconnect') +def test_disconnect(): + global client_count + client_count -= 1 + + +if __name__ == '__main__': + socketio.run(app) From 33559b2d572fa92efc7afdf5d7b714c670d82f83 Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 29 Jun 2019 12:41:00 -0700 Subject: [PATCH 5/9] Add argparse to specify host name in CLI --- controller-client.py | 7 ++++--- controller-remote.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/controller-client.py b/controller-client.py index ab74ec2..abcc245 100644 --- a/controller-client.py +++ b/controller-client.py @@ -6,6 +6,7 @@ # standard Python sio = socketio.Client() +host = None connected = False ser = None @@ -41,14 +42,13 @@ def disconnect(): while num_tries < 5: try: print('attempting to reconnect...') - sio.connect('http://localhost:5000') + sio.connect(host) except ConnectionError: print('failed...') time.sleep(5) if __name__ == '__main__': - parser = argparse.ArgumentParser() parser.add_argument('-h', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') parser.add_argument('-c', '--controller', type=str, default='0', help='Controller to use. Default: 0.') @@ -56,8 +56,9 @@ def disconnect(): parser.add_argument('-p', '--port', type=str, default='/dev/ttyUSB0', help='Serial port. Default: /dev/ttyUSB0.') args = parser.parse_args() + host = args.host - sio.connect(args.host) + sio.connect(host) ser = serial.Serial(args.port, args.baud_rate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=None) print('Using {:s} at {:d} baud for comms.'.format(args.port, args.baud_rate)) diff --git a/controller-remote.py b/controller-remote.py index 47a87c3..eb0ac32 100644 --- a/controller-remote.py +++ b/controller-remote.py @@ -2,10 +2,12 @@ from socketio.exceptions import ConnectionError import bridge import sdl2 +import argparse # standard Python sio = socketio.Client() connected = False +host = None @sio.event @@ -25,7 +27,7 @@ def disconnect(): while num_tries < 5: try: print('attempting to reconnect...') - sio.connect('http://localhost:5000') + sio.connect(host) except ConnectionError: print('failed...') time.sleep(5) @@ -44,5 +46,10 @@ def init_input_loop(joystick_idx): if __name__ == '__main__': - sio.connect('http://localhost:5000') - init_input_loop('0') + parser = argparse.ArgumentParser() + parser.add_argument('-h', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') + parser.add_argument('-c', '--controller', type=str, default='0', help='SDL2 Controller Index') + args = parser.parse_args() + host = args.host + sio.connect(host) + init_input_loop(args.controller) From 279e9372a85a50c63033735fd7abff6a85546b85 Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 29 Jun 2019 13:21:44 -0700 Subject: [PATCH 6/9] Argparse typo --- controller-client.py | 2 +- controller-remote.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller-client.py b/controller-client.py index abcc245..3d99c8d 100644 --- a/controller-client.py +++ b/controller-client.py @@ -50,7 +50,7 @@ def disconnect(): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('-h', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') + parser.add_argument('-H', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') parser.add_argument('-c', '--controller', type=str, default='0', help='Controller to use. Default: 0.') parser.add_argument('-b', '--baud-rate', type=int, default=115200, help='Baud rate. Default: 115200.') parser.add_argument('-p', '--port', type=str, default='/dev/ttyUSB0', help='Serial port. Default: /dev/ttyUSB0.') diff --git a/controller-remote.py b/controller-remote.py index eb0ac32..2509efd 100644 --- a/controller-remote.py +++ b/controller-remote.py @@ -47,7 +47,7 @@ def init_input_loop(joystick_idx): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('-h', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') + parser.add_argument('-H', '--host', type=str, default='http://localhost:5000', help='Websocket Server Host.') parser.add_argument('-c', '--controller', type=str, default='0', help='SDL2 Controller Index') args = parser.parse_args() host = args.host From de84b2f04832ee7c96b0ad34f56a52fd76d2179a Mon Sep 17 00:00:00 2001 From: ahmed Date: Sat, 29 Jun 2019 21:36:31 -0700 Subject: [PATCH 7/9] Insert smashbros macros --- smashbros_controller.py | 226 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 smashbros_controller.py diff --git a/smashbros_controller.py b/smashbros_controller.py new file mode 100644 index 0000000..1c2d44d --- /dev/null +++ b/smashbros_controller.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + + +import argparse +from contextlib import contextmanager + +import sdl2 +import sdl2.ext +import struct +import binascii +import serial +import math +import datetime as dt +import pickle + +from tqdm import tqdm +from collections import namedtuple +from enum import IntEnum + +from bridge import InputStack, ControllerStateTime, controller_states +import bridge + + +# NOTE: SCREEN START IN 'GAMES & MORE' then goes to training, and the cursor is on the practice stage. + +ser = serial.Serial('/dev/ttyUSB0', 115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, timeout=None) + + +def message(lx=128, ly=128, rx=128, ry=128, *inputs): + inputs = set(inputs) + + buttons = sum([(b in inputs) << n for n, b in enumerate(bridge.buttonmapping)]) + buttons |= (32767 if sdl2.SDL_CONTROLLER_AXIS_TRIGGERLEFT in inputs else 0) << 6 + buttons |= (32767 if sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT in inputs else 0) << 7 + + hat = bridge.hatcodes[sum([(b in inputs) << n for n, b in enumerate(bridge.hatmapping)])] + return struct.pack('>BHBBBB', hat, buttons, lx, ly, rx, ry) + + +class ContinuousAction: + def __init__(self, title=None): + + self.elapsed_sec = 0 + self.actions = [] + self.actions.append(None) + if title: + self.actions.append('---' + '\n' + title.upper() + '\n' + '---') + + def hold(self, msg, secs): + self.actions.append(ControllerStateTime(msg, self.elapsed_sec)) + self.actions.append(ControllerStateTime(message(), self.elapsed_sec + secs)) + self.elapsed_sec += secs + return self + + def press(self, msg): + self.actions.append(ControllerStateTime(msg, self.elapsed_sec)) + self.actions.append(ControllerStateTime(message(), self.elapsed_sec + 0.1)) + self.elapsed_sec += 0.05 + return self + + def wait(self, secs): + self.elapsed_sec += secs + return self + + def log(self, string): + self.actions.append(string) + return self + + def play(self): + for m in self.actions: + if isinstance(m, str): + print('> ', m) + else: + yield m + + +class StageMode(IntEnum): + STANDARD = 1 + BATTLEFIELD = 1 + FINAL_DESTINATION = 2 + + +def menu_nav(row, col, stage_mode=StageMode.STANDARD): + action = ContinuousAction(f"NAVIGATING TO STAGE: [{row}, {col}] in {stage_mode.name}") + + col_time = 0.232 + row_time = 0.21 + + action.log("Go back one screen") + action.hold(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_A), 3) + action.wait(2) + + action.log("Forward one screen to reset the cursor to practice-stage") + action.press(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_B)) + action.wait(3) + + action.log("navigate stage-select matrix") + for _ in range(col): + action.hold(message(255, 128), col_time) + action.wait(0.1) + for _ in range(row): + action.hold(message(128, 255), row_time) + action.wait(0.1) + + action.log("Choose stage mode") + for _ in range(stage_mode): + action.press(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_Y)) + action.wait(0.1) + + action.log("Done!") + action.hold(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_B), 6) + return action + + +def character_nav(row, col, reset_cursor=True): + action = ContinuousAction(f"NAVIGATING TO CHARACTER: [{row}, {col}]") + + row_time = 0.49 + col_time = 0.27 + + if reset_cursor: + action.log("Go back one screen") + action.hold(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_A), 3) + action.wait(2) + + action.log("Forward one screen to reset the cursor to the center of player 1") + action.press(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_B)) + action.wait(6) + else: + action.wait(1) + + action.log("navigate to Mario") + action.hold(message(128, 0), 0.675) + action.wait(0.2) + action.hold(message(0, 128), col_time) + action.wait(0.2) + action.hold(message(0, 128), col_time) + action.wait(0.2) + + action.log("Navigate character-select matrix") + for _ in range(row): + action.hold(message(128, 192), row_time) + action.wait(0.2) + if row == 5: + action.log("Compensate for centered grid. Navigate to Ridley.") + action.hold(message(255, 128), .32) + action.wait(0.2) + for _ in range(col): + action.hold(message(255, 128), col_time) + action.wait(0.2) + + action.press(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_B)) + action.hold(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_START), 6) + action.hold(message(128, 128, 128, 128, sdl2.SDL_CONTROLLER_BUTTON_START), 6) + + return action + + +def play_file(file_path, playback_speed=1): + yield None + for timestamp in bridge.replay_states(file_path): + yield ControllerStateTime(timestamp.message, timestamp.delta / playback_speed) + + +def play_actions(*args): + input_stack = InputStack() + for a in args[::-1]: + input_stack.push(a) + + start_dttm = dt.datetime.now().timestamp() + prev_msg_stamp = None + while True: + try: + sdl2.ext.get_events() + msg_stamp = next(input_stack) + if msg_stamp is None: + start_dttm = dt.datetime.now().timestamp() + continue + # This this input has aleady been entered, then don't spam the stack + if prev_msg_stamp and msg_stamp.message == prev_msg_stamp.message: + continue + # Wait for the correct amount of time to pass before performing an input + while True: + elapsed_delta = dt.datetime.now().timestamp() - start_dttm + if msg_stamp.delta < elapsed_delta: + break + ser.write(msg_stamp.formatted_message()) + prev_msg_stamp = msg_stamp + except StopIteration: + break + + while True: + # wait for the arduino to request another state. + response = ser.read(1) + if response == b'U': + break + elif response == b'X': + print('Arduino reported buffer overrun.') + + +def reset_practice(): + action = ContinuousAction(f"RESETING") + action.press(message( + 128, 128, 128, 128, + sdl2.SDL_CONTROLLER_BUTTON_LEFTSHOULDER, # L + sdl2.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, # R + sdl2.SDL_CONTROLLER_BUTTON_B # A + )) + return action + + +if __name__ == '__main__': + import os + macros_dir = '/home/awkii/macros' + play_actions( + menu_nav(1, 5, stage_mode=StageMode.FINAL_DESTINATION).play(), + character_nav(1, 12).play(), + # reset_practice().play(), + # play_file(f'{macros_dir}/pit/bair.map'), + # reset_practice().play(), + # play_file(f'{macros_dir}/pit/dsmash.map'), + # reset_practice().play(), + + controller_states('0') + ) From 7128e441a283a689a13363efe108b3ffb18364b6 Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 29 Jun 2019 22:56:30 -0700 Subject: [PATCH 8/9] Allow for slow-start with controller-remote --- controller-remote.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/controller-remote.py b/controller-remote.py index 2509efd..f21a13c 100644 --- a/controller-remote.py +++ b/controller-remote.py @@ -2,6 +2,7 @@ from socketio.exceptions import ConnectionError import bridge import sdl2 +import time import argparse # standard Python @@ -34,13 +35,15 @@ def disconnect(): def init_input_loop(joystick_idx): - inputs = bridge.controller_states(joystick_idx) + inputs = bridge.controller_states(joystick_idx, force_axis=True) prev_message = None + time.sleep(1) while connected: sdl2.ext.get_events() message_stamp = next(inputs) - message = message_stamp.formatted_message() + message = message_stamp.message if message != prev_message: + print(message) sio.emit('controller-input', message) prev_message = message From 7f49788f9253078ee4786e5b4d4f667d645e8cea Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Sat, 29 Jun 2019 22:57:12 -0700 Subject: [PATCH 9/9] Allow for subroutine calls to record from webcam during macro inputs --- recorder.py | 113 ++++++++++++++++++++++++++++++++++++++++ smashbros_controller.py | 19 +++---- 2 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 recorder.py diff --git a/recorder.py b/recorder.py new file mode 100644 index 0000000..4748cf8 --- /dev/null +++ b/recorder.py @@ -0,0 +1,113 @@ +import os +import signal +import psutil +import errno +import subprocess + + +def reap_process_group(pid, sig=signal.SIGTERM, timeout=60): + """ + Note: Shamelessly Stolen from Apache-Airflow... + + Tries really hard to terminate all children (including grandchildren). Will send + sig (SIGTERM) to the process group of pid. If any process is alive after timeout + a SIGKILL will be send. + + :param log: log handler + :param pid: pid to kill + :param sig: signal type + :param timeout: how much time a process has to terminate + """ + + def on_terminate(p): + print("Process %s (%s) terminated with exit code %s" % (p, p.pid, p.returncode)) + + if pid == os.getpid(): + raise RuntimeError("I refuse to kill myself") + + parent = psutil.Process(pid) + + children = parent.children(recursive=True) + children.append(parent) + + try: + pg = os.getpgid(pid) + except OSError as err: + # Skip if not such process - we experience a race and it just terminated + if err.errno == errno.ESRCH: + return + raise + + print("Sending %s to GPID %s" % (sig, pg)) + os.killpg(os.getpgid(pid), sig) + + gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) + + if alive: + for p in alive: + print("process %s (%s) did not respond to SIGTERM. Trying SIGKILL", p, pid) + + os.killpg(os.getpgid(pid), signal.SIGKILL) + + gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate) + if alive: + for p in alive: + print("Process %s (%s) could not be killed. Giving up." % (p, p.pid)) + + +def start_webcam_recording(file): + cmd = [ + 'ffmpeg', + '-video_size', + '1280x720', + '-framerate', + '30', + '-f', + 'avfoundation', + '-i', + '"default"', + file + ] + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + close_fds=True, + env=os.environ.copy(), + preexec_fn=os.setsid + ) + + return proc + + +def stop_webcam_recording(proc): + reap_process_group(proc.pid) + + +def get_recorder_functions(filename): + """ + Returns 2 functions that will start & stop recording to a filename & take no parameters + :return: + """ + proc = None + + def start_webcam(): + nonlocal proc + proc = start_webcam_recording(filename) + + def stop_webcam(): + stop_webcam_recording(proc) + return start_webcam, stop_webcam + + +if __name__ == '__main__': + import time + start, stop = get_recorder_functions('example.mpg') + print('Starting Recording') + start() + time.sleep(10) + print('Stopping Recording') + stop() + diff --git a/smashbros_controller.py b/smashbros_controller.py index 1c2d44d..14786c7 100644 --- a/smashbros_controller.py +++ b/smashbros_controller.py @@ -1,20 +1,10 @@ #!/usr/bin/env python3 - - -import argparse -from contextlib import contextmanager - import sdl2 import sdl2.ext import struct -import binascii import serial -import math import datetime as dt -import pickle -from tqdm import tqdm -from collections import namedtuple from enum import IntEnum from bridge import InputStack, ControllerStateTime, controller_states @@ -40,7 +30,6 @@ def message(lx=128, ly=128, rx=128, ry=128, *inputs): class ContinuousAction: def __init__(self, title=None): - self.elapsed_sec = 0 self.actions = [] self.actions.append(None) @@ -67,10 +56,17 @@ def log(self, string): self.actions.append(string) return self + def subroutine(self, function): + self.actions.append(function) + return self + def play(self): for m in self.actions: if isinstance(m, str): print('> ', m) + elif isinstance(m, callable): + print(f'> Running subroutine {m.__name__}') + m() else: yield m @@ -211,7 +207,6 @@ def reset_practice(): if __name__ == '__main__': - import os macros_dir = '/home/awkii/macros' play_actions( menu_nav(1, 5, stage_mode=StageMode.FINAL_DESTINATION).play(),