Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 51 additions & 20 deletions bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:')
Expand Down Expand Up @@ -74,6 +77,8 @@ def get_controller(c):
axis_deadzone = 1000
trigger_deadzone = 0

start_dttm = dt.datetime.now().timestamp()


def controller_states(controller_id):

Expand All @@ -88,22 +93,26 @@ def controller_states(controller_id):
print('Using controller {:s} for input.'.format(controller_id))

while True:
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
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)<<n for n,b in enumerate(hatmapping)])]
hat = hatcodes[sum([sdl2.SDL_GameControllerGetButton(controller, b) << n for n, b in enumerate(hatmapping)])]

rawaxis = [sdl2.SDL_GameControllerGetAxis(controller, n) for n in axismapping]
axis = [((0 if abs(x) < axis_deadzone else x) >> 8) + 128 for x in rawaxis]

rawbytes = struct.pack('>BHBBBB', hat, buttons, *axis)
yield binascii.hexlify(rawbytes) + b'\n'
message_stamp = ControllerStateTime(rawbytes, elaped_time)
yield message_stamp


def replay_states(filename):
with open(filename, 'rb') as replay:
yield from replay.readlines()
for line in replay.readlines():
# remove new-line character at end of line, and feed it into deserializer
yield ControllerStateTime.deserialize(line[:-1])


def example_macro():
Expand All @@ -112,12 +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 binascii.hexlify(rawbytes) + b'\n'


message_stamp = ControllerStateTime(rawbytes, elapsed_time)
yield message_stamp


class InputStack(object):
Expand All @@ -143,6 +152,20 @@ def __next__(self):
raise StopIteration


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'

def serialize(self):
return binascii.hexlify(pickle.dumps(self))

@staticmethod
def deserialize(self):
return pickle.loads(binascii.unhexlify(self))



if __name__ == '__main__':
Expand Down Expand Up @@ -171,39 +194,47 @@ 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))

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:
message = next(input_stack)
ser.write(message)
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(message)
record.write(msg_stamp.serialize() + b'\n')
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.formatted_message()[:-1].decode('utf8')))
pbar.update()

while True:
Expand Down
65 changes: 65 additions & 0 deletions controller-client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import socketio
from socketio.exceptions import ConnectionError
import bridge
import serial
import argparse

# standard Python
sio = socketio.Client()
host = None
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(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.')
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()
host = 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))

58 changes: 58 additions & 0 deletions controller-remote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import socketio
from socketio.exceptions import ConnectionError
import bridge
import sdl2
import time
import argparse

# standard Python
sio = socketio.Client()
connected = False
host = None


@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(host)
except ConnectionError:
print('failed...')
time.sleep(5)


def init_input_loop(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.message
if message != prev_message:
print(message)
sio.emit('controller-input', message)
prev_message = message


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='SDL2 Controller Index')
args = parser.parse_args()
host = args.host
sio.connect(host)
init_input_loop(args.controller)
40 changes: 40 additions & 0 deletions controller-server.py
Original file line number Diff line number Diff line change
@@ -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)
Loading