From 22cdc98d6cc431ca0f0c7f371060e0c2b83e0adb Mon Sep 17 00:00:00 2001 From: Brikwerk Date: Sat, 12 Sep 2020 22:04:25 -0700 Subject: [PATCH] Added webapp functionality --- nxbt/web/app.py | 67 ++- nxbt/web/static/css/main.css | 636 +++++++++++++++++++++++ nxbt/web/static/favicon.png | Bin 0 -> 26906 bytes nxbt/web/static/joyconl.svg | 1 + nxbt/web/static/joyconr.svg | 1 + nxbt/web/static/js/main.js | 676 +++++++++++++++++++++++++ nxbt/web/static/procon-transparent.svg | 1 + nxbt/web/static/procon.svg | 1 + nxbt/web/templates/index.html | 267 +++++++--- 9 files changed, 1581 insertions(+), 69 deletions(-) create mode 100644 nxbt/web/static/css/main.css create mode 100644 nxbt/web/static/favicon.png create mode 100644 nxbt/web/static/joyconl.svg create mode 100644 nxbt/web/static/joyconr.svg create mode 100644 nxbt/web/static/js/main.js create mode 100644 nxbt/web/static/procon-transparent.svg create mode 100644 nxbt/web/static/procon.svg diff --git a/nxbt/web/app.py b/nxbt/web/app.py index 1f6e552..0712eaf 100644 --- a/nxbt/web/app.py +++ b/nxbt/web/app.py @@ -1,8 +1,10 @@ import json import os +from threading import RLock +from time import perf_counter -from ..nxbt import Nxbt -from flask import Flask, render_template +from ..nxbt import Nxbt, PRO_CONTROLLER +from flask import Flask, render_template, request from flask_socketio import SocketIO, emit import eventlet @@ -29,6 +31,9 @@ app.config['SECRET_KEY'] = secret_key # Starting socket server with Flask app sio = SocketIO(app, cookie=False) +user_info_lock = RLock() +USER_INFO = {} + @app.route('/') def index(): @@ -37,24 +42,70 @@ def index(): @sio.on('connect') def on_connect(): - print("Connected") - index = nxbt.create_controller() + with user_info_lock: + USER_INFO[request.sid] = {} + + +@sio.on('state') +def on_state(): + state_proxy = nxbt.state.copy() + state = {} + for controller in state_proxy.keys(): + state[controller] = state_proxy[controller].copy() + emit('state', state) @sio.on('disconnect') def on_disconnect(): print("Disconnected") + with user_info_lock: + try: + index = USER_INFO[request.sid]["controller_index"] + nxbt.remove_controller(index) + except KeyError: + pass -@sio.on('create_controller') +@sio.on('shutdown') +def on_shutdown(index): + nxbt.remove_controller(index) + + +@sio.on('create_pro_controller') def on_create_controller(): - pass + print("Create Controller") + + try: + reconnect_addresses = nxbt.get_switch_addresses() + index = nxbt.create_controller(PRO_CONTROLLER, reconnect_address=reconnect_addresses) + + with user_info_lock: + USER_INFO[request.sid]["controller_index"] = index + + emit('create_pro_controller', index) + except Exception as e: + emit('error', str(e)) @sio.on('input') def handle_input(message): - print(json.loads(message)) + message = json.loads(message) + index = message[0] + input_packet = message[1] + nxbt.set_controller_input(index, input_packet) + + +@sio.on('macro') +def handle_macro(message): + message = json.loads(message) + index = message[0] + macro = message[1] + nxbt.macro(index, macro) def start_web_app(): - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + eventlet.wsgi.server(eventlet.listen(('127.0.0.1', 8000)), app) + + +if __name__ == "__main__": + start_web_app() diff --git a/nxbt/web/static/css/main.css b/nxbt/web/static/css/main.css new file mode 100644 index 0000000..1df4b90 --- /dev/null +++ b/nxbt/web/static/css/main.css @@ -0,0 +1,636 @@ +/************************************************** +General Stylings +**************************************************/ + +html, body { + padding: 0; + margin: 0; + background-color: #1a1f28; + width: 100%; + height: 100%; + display: block; +} + +* { + color: #d9dada; + font-family: Helvetica, Arial, sans-serif; +} + +main { + max-width: 1200px; + margin: auto; + height: 100%; + width: 100%; + padding-top: 5rem; + box-sizing: border-box; +} + +footer { + text-align: center; + padding: 1rem 4rem; +} + +footer p, footer a { + margin: 0; + color: #3f5066; +} + +h1 { + font-weight: 700; +} + +p { + padding: 0; + line-height: 1.4; +} + +button { + color: #1a1f28; + padding: 1rem; + border-radius: 5px; + margin: auto; + font-weight: 700; + text-transform: uppercase; + display: block; + -webkit-appearance: none; + background-color: #d9dada; + font-size: 0.9rem; +} + +button:hover { + cursor: pointer; +} + +.mono { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace !important +} + +.surface { + display: block; + background-color: #2c3544; + border-radius: 5px; + box-shadow: 0px 1px 2px -1px rgba(0,0,0,0.2), 0px 4px 2px 0px rgba(0,0,0,0.14); +} + +.surface-lighter { + display: block; + background-color: #3a4759; + border-radius: 5px; + box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12) +} + +.hidden { + display: none !important; +} + +.text-section p { + text-align: left; +} + +/************************************************** +Top Bar Stying +**************************************************/ + +.top-bar { + display: block; + position: fixed; + width: 100%; + background-color: #2c3544; + box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14); + z-index: 100; +} + +.top-bar-wrapper { + background-color: #2c3544; + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1200px; + margin: auto; + padding: 1rem 2rem; +} + +.top-bar-wrapper h1 { + margin: 0; + display: inline-block; +} + +.top-bar-wrapper .mono { + display: inline-block; +} + +.top-bar-spacer { + width: 100%; + height: 5rem; +} + +/************************************************** +Status Indicator Stying +**************************************************/ + +#status-indicator { + display: block; + background-color: #3a4759; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border-radius: 5px; + position: relative; +} + +#status-indicator-text { + font-size: 1rem; + text-transform: uppercase; +} + +#status-indicator-light { + border-radius: 100%; + height: 0.8rem; + width: 0.8rem; + position: absolute; + left: 1rem; + top: 0.8rem; +} + +.indicator-green { + background-color: #5fc54f; + border: 1px solid #55a83d; +} + +.indicator-yellow { + background-color: #f6be45; + border: 1px solid #d9a33b; +} + +.indicator-red { + background-color: #ef6b5c; + border: 1px solid #d25647; +} + +/************************************************** +Controller Selection Stying +**************************************************/ + +#controller-selection { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + align-content: center; + justify-content: center; + width: 100%; + text-align: center; + box-sizing: border-box; + padding-top: 2.5%; + padding-bottom: 2.5%; +} + +#controller-selection h1 { + width: 100%; + margin: 0.5rem 0; +} + +.controller-option { + position: relative; + width: 18rem; + padding: 0 0 1.5rem 0; + margin: 1rem; + display: inline-block; + font-weight: 500; + text-align: center; + text-transform: uppercase; + transition: 0.1s; + bottom: 0px; +} + +.controller-option:hover { + cursor: pointer; + bottom: 5px; + transition: 0.1s; +} + +.controller-option-img-wrapper { + display: block; + background-color: #3a4759; + width: 100%; + padding: 2rem 0; + border-radius: 5px; + margin-bottom: 1.5rem; +} + +.controller-option-img-wrapper img { + max-height: 10rem; + display: block; + width: 100%; +} + +@media screen and (max-width: 960px) { + #controller-selection { + display: block; + } + + .controller-option { + width: 90%; + margin: 0.5rem; + } + + .controller-option-img-wrapper img { + max-height: 5rem; + } +} + +/************************************************** +Loader Stying +**************************************************/ + +#loader { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + align-content: center; + justify-content: center; + min-height: 40%; + width: 100%; + text-align: center; + box-sizing: border-box; + margin-top: 10%; + padding: 2rem 20%; +} + +#loader-progress-wrapper { + width: 100%; + padding: 2rem 1rem; +} + +#loader-blocks { + display: flex; + align-items: center; + justify-content: space-between; + width: 8rem; + margin: auto; +} + +.loader-block { + width: 1rem; + height: 1rem; + background-color: #1a1f28; +} + +.loader-block-highlight { + background-color: #5fc54f !important; +} + +#loader-text { + width: 100%; + font-size: 1.2rem; + text-transform: uppercase; + margin-top: 1rem; + padding: 1rem; +} + +#loader .text-section { + padding: 1.5rem 2rem; + margin-top: 2rem; + border-radius: 5px; + background-color: #1a1f28; +} + +#loader .text-section h2 { + margin-top: 0; + margin: auto; + width: 50%; + background-color: #2c3544; + padding: 0.5rem; + border-radius: 5px; + font-size: 1.2rem; + box-sizing: border-box; +} + +#loader .text-section .tip-box { + width: 100%; + text-align: left; + font-weight: 500; + font-size: 1.1rem; + margin-top: 1rem; + background-color: #1a1f28; +} + +@media screen and (max-width: 960px) { + #loader { + padding-left: 5%; + padding-right: 5%; + } +} + +/************************************************** +Controller Config Stying +**************************************************/ + +#controller-config { + padding: 2rem; + margin-top: 3rem; + box-sizing: border-box; + text-align: center; +} + +#controller-underlay-container { + box-sizing: border-box; + background-color: #1a1f28; + border-radius: 5px; + padding: 2rem 10%; +} + +.controller-underlay { + width: 100%; + display: block; + z-index: 2; + position: relative; +} + +.controller-display { + width: 100%; + max-width: 25rem; + margin: auto; + display: block; + position: relative; +} + +.control-indicator { + display: block; + position: absolute; + width: 6%; + padding-top: 6%; + border-radius: 100%; + background-color: #647f96; + z-index: 1; +} + +.square-button { + border-radius: 0px !important; +} + +.big-button { + width: 8% !important; + padding-top: 8% !important; +} + +/** Pro Controller Control Indicator Positioning **/ +#pc_ls { + left: 19.25%; /* LM: 15.5, RM: 23*/ + top: 23.75%; /* TM: 18, BM: 29*/ } +#pc_rs { + left: 59.75%; /* LM: 56, RM: 63.5*/ + top: 44.125%; /* TM: 38.125, BM: 50*/ } +#pc_du { + left: 30.7%; + top: 36%; } +#pc_dl { + left: 25.8%; + top: 45.2%; } +#pc_dr { + left: 36.8%; + top: 44.4%; } +#pc_dd { + left: 31.3%; + top: 52.4%; } +#pc_h { + left: 54%; + top: 24%; } +#pc_c { + left: 40%; + top: 24%; } +#pc_m { + left: 59%; + top: 12%; } +#pc_p { + left: 35%; + top: 13%; } +#pc_b { + left: 72%; + top: 32%; } +#pc_a { + left: 80%; + top: 22%; } +#pc_y { + left: 64%; + top: 22%; } +#pc_x { + left: 72%; + top: 12%; } +#pc_l { + left: 10%; + top: 2%; } +#pc_zl { + left: 17%; + top: -2%; } +#pc_r { + left: 84%; + top: 2%; } +#pc_zr { + left: 77%; + top: -2%; } + +.controller-config-subsection { + padding: 1rem; + margin-top: 2%; + display: inline-block; + min-width: 48.75%; + max-width: 48.75%; + box-sizing: border-box; + vertical-align: top; +} + +.controller-config-subsection h2 { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + font-weight: 500; + margin-top: 2rem; + font-size: 1.25rem; + border-radius: 5px; +} + +.controller-config-subsection h2:first-child { + margin-top: 0; +} + +.controller-config-subsection p { + text-align: left; + padding: 0.25rem; +} + +.controller-config-subsection select { + width: 99%; + padding: 0.25rem; + color: #1a1f28; +} + +.controller-config-subsection:nth-child(even) { + margin-right: 1%; +} + +.controller-config-subsection:nth-child(odd) { + margin-left: 1%; +} + +.controller-config-subsection table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; +} + +.controller-config-subsection td { + border-top: 2px solid #2c3544; + border-bottom: 2px solid #2c3544; + box-sizing: border-box; + padding: 1rem; +} + +.controller-config-subsection td:nth-child(1) { + text-align: left; +} + +.controller-config-subsection td:nth-child(2) { + text-align: right; +} + +.controller-config-subsection button { + width: 100%; + margin-bottom: 0.25rem; + margin-left: auto; + margin-right: auto; +} + +#controller-map { + padding: 1rem; +} + +.controller-config-subsection button:last-child { + margin-bottom: 0; +} + +.controller-config-subsection textarea { + width: 98%; + margin: auto; + min-height: 10rem; + color: black; + margin-bottom: 0.25rem; + text-transform: uppercase; + box-sizing: border-box; + padding: 1rem; +} + +@media screen and (max-width: 960px) { + #controller-config { + padding-left: 1rem; + padding-right: 1rem; + margin-top: 1rem; + } + + .controller-config-subsection { + width: 100%; + max-width: 100%; + margin-top: 4%; + padding-left: 0; + padding-right: 0; + } + + .controller-config-subsection:nth-child(even) { + margin-right: 0; + } + + .controller-config-subsection:nth-child(odd) { + margin-left: 0; + } +} + +/************************************************** +Controller Sessions Stying +**************************************************/ + +#controller-sessions { + display: block; + padding: 2rem; + max-width: 100%; + margin: auto; + margin-top: 1rem; +} + +#controller-sessions h1 { + margin-top: 0 +} + +.controller-session { + box-sizing: border-box; + padding: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #1a1f28; + border-radius: 5px; +} + +.controller-session h1 { + font-size: 1.3rem; + padding: 0; + margin: 0; + display: inline-block; + text-align: left; +} + +.controller-session button { + font-size: 1rem; + padding: 0.75rem 1rem; + margin: 0; + display: inline-block; +} + +@media screen and (max-width: 960px) { + #controller-sessions { + padding-left: 1rem; + padding-right: 1rem; + } +} + +/************************************************** +Error Display Stying +**************************************************/ + +#error-display { + width: 80%; + max-width: 960px; + display: block; + position: fixed; + bottom: 0; + text-align: left; + left: 50%; + transform: translateX(-50%); + box-sizing: border-box; + padding: 1rem; +} + +.error { + border-radius: 5px; + background-color: #d25647; + padding: 1rem 1.5rem; + box-sizing: border-box; + margin-bottom: 0.5rem; + transition: 0.25s; +} + +.error h1 { + padding: 0; + margin: 0; + font-size: 1.1rem; + color: #d9dada; + text-transform: uppercase; + font-weight: bold; + display: inline; + padding-right: 0.5rem; +} + +.error p { + padding: 0; + margin: 0; + font-size: 1.1rem; + display: inline; + color: #d9dada; +} diff --git a/nxbt/web/static/favicon.png b/nxbt/web/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..988e2c1c56750eb7ab48ce88b9a7ea7fca00ea04 GIT binary patch literal 26906 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 ANB{r; literal 0 HcmV?d00001 diff --git a/nxbt/web/static/joyconl.svg b/nxbt/web/static/joyconl.svg new file mode 100644 index 0000000..d963b82 --- /dev/null +++ b/nxbt/web/static/joyconl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nxbt/web/static/joyconr.svg b/nxbt/web/static/joyconr.svg new file mode 100644 index 0000000..1cf2f34 --- /dev/null +++ b/nxbt/web/static/joyconr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nxbt/web/static/js/main.js b/nxbt/web/static/js/main.js new file mode 100644 index 0000000..b944492 --- /dev/null +++ b/nxbt/web/static/js/main.js @@ -0,0 +1,676 @@ +/**********************************************/ +/* Globals, Constants, and Enums */ +/**********************************************/ + +let NXBT_CONTROLLER_INDEX = false; +let CONTROLLER_INDEX = false; +let CONTROLLER_CONNECTED = false; +let STATE = false; + +// HTML SECTIONS +let HTML_CONTROLLER_SELECTION = document.getElementById("controller-selection"); +let HTML_LOADER = document.getElementById("loader"); +let HTML_LOADER_TEXT = document.getElementById("loader-text"); +let HTML_CONTROLLER_CONFIG = document.getElementById("controller-config"); +let HTML_MACRO_TEXT = document.getElementById("macro-text"); +let HTML_STATUS_INDICATOR = document.getElementById("status-indicator"); +let HTML_STATUS_INDICATOR_LIGHT = document.getElementById("status-indicator-light"); +let HTML_STATUS_INDICATOR_TEXT = document.getElementById("status-indicator-text"); +let HTML_KEYBOARD_MAP = document.getElementById("keyboard-map"); +let HTML_CONTROLLER_MAP = document.getElementById("controller-map"); +let HTML_ERROR_DISPLAY = document.getElementById("error-display"); +let HTML_CONTROLLER_SESSIONS = document.getElementById("controller-sessions"); +let HTML_CONTROLLER_SESSIONS_CONTAINER = document.getElementById("controller-session-container"); + +const ControllerState = { + INITIALIZING: "initializing", + CONNECTING: "connecting", + RECONNECTING: "reconnecting", + CONNECTED: "connected", + CRASHED: "crashed", +} + +const InputDevice = { + KEYBOARD: "keyboard", + GAMEPAD: "gamepad" +} +let INPUT_DEVICE = InputDevice.KEYBOARD; + +const ControllerType = { + PRO_CONTROLLER: "pro_controller", + JOYCON_L: "joycon_l", + JOYCON_R: "joycon_r" +} + +const KEYMAP = { + // Left Stick + 87: "LS_UP", + 65: "LS_LEFT", + 68: "LS_RIGHT", + 83: "LS_DOWN", + 84: "LS_PRESS", + // Right Stick + 38: "RS_UP", + 37: "RS_LEFT", + 39: "RS_RIGHT", + 40: "RS_DOWN", + 89: "RS_PRESS", + // Dpad + 71: "DPAD_UP", + 86: "DPAD_LEFT", + 78: "DPAD_RIGHT", + 66: "DPAD_DOWN", + // Home & Capture + 219: "HOME", + 221: "CAPTURE", + // Plus & Minus + 54: "PLUS", + 55: "MINUS", + // A B X Y + 76: "A", + 75: "B", + 73: "X", + 74: "Y", + // L & ZL + 49: "L", + 50: "ZL", + // R & ZR + 56: "ZR", + 57: "R", +} + +const LEFT_STICK = [ + "LS_UP", + "LS_LEFT", + "LS_RIGHT", + "LS_DOWN" +] +const RIGHT_STICK = [ + "RS_UP", + "RS_LEFT", + "RS_RIGHT", + "RS_DOWN" +] + +let INPUT_PACKET = { + // Sticks + "L_STICK": { + "PRESSED": false, + "X_VALUE": 0, + "Y_VALUE": 0, + // Keyboard position calculation values + "LS_UP": false, + "LS_LEFT": false, + "LS_RIGHT": false, + "LS_DOWN": false + }, + "R_STICK": { + "PRESSED": false, + "X_VALUE": 0, + "Y_VALUE": 0, + // Keyboard position calculation values + "RS_UP": false, + "RS_LEFT": false, + "RS_RIGHT": false, + "RS_DOWN": false + }, + // Dpad + "DPAD_UP": false, + "DPAD_LEFT": false, + "DPAD_RIGHT": false, + "DPAD_DOWN": false, + // Triggers + "L": false, + "ZL": false, + "R": false, + "ZR": false, + // Joy-Con Specific Buttons + "JCL_SR": false, + "JCL_SL": false, + "JCR_SR": false, + "JCR_SL": false, + // Meta buttons + "PLUS": false, + "MINUS": false, + "HOME": false, + "CAPTURE": false, + // Buttons + "Y": false, + "X": false, + "B": false, + "A": false +} +let INPUT_PACKET_OLD = JSON.parse(JSON.stringify(INPUT_PACKET)); + +let PRO_CONTROLLER_DISPLAY = { + // Sticks + "L_STICK": { + "STICK": true, + "ELEMENT": document.getElementById("pc_ls"), + "MAX_X": 23, + "MIN_X": 15.5, + "DIFF_X": 7.5, + "MAX_Y": 29, + "MIN_Y": 18, + "DIFF_Y": 11 + }, + "R_STICK": { + "STICK": true, + "ELEMENT": document.getElementById("pc_rs"), + "MAX_X": 63.5, + "MIN_X": 56, + "DIFF_X": 7.5, + "MAX_Y": 50, + "MIN_Y": 38.125, + "DIFF_Y": 11.875, + }, + // Dpad + "DPAD_UP": document.getElementById("pc_du"), + "DPAD_LEFT": document.getElementById("pc_dl"), + "DPAD_RIGHT": document.getElementById("pc_dr"), + "DPAD_DOWN": document.getElementById("pc_dd"), + // Triggers + "L": document.getElementById("pc_l"), + "ZL": document.getElementById("pc_zl"), + "R": document.getElementById("pc_r"), + "ZR": document.getElementById("pc_zr"), + // Meta buttons + "PLUS": document.getElementById("pc_p"), + "MINUS": document.getElementById("pc_m"), + "HOME": document.getElementById("pc_h"), + "CAPTURE": document.getElementById("pc_c"), + // Buttons + "Y": document.getElementById("pc_y"), + "X": document.getElementById("pc_x"), + "B": document.getElementById("pc_b"), + "A": document.getElementById("pc_a") +} + +/**********************************************/ +/* Socket.IO Functionality */ +/**********************************************/ + +let socket = io(); + +// Request to the state at 1Hz +socket.emit('state'); +let stateInterval = setInterval(function() { + socket.emit('state'); +}, 1000); + +socket.on('state', function(state) { + STATE = state; +}); + +socket.on('connect', function() { + console.log("Connected"); +}); + +checkForLoadInterval = false; +socket.on('create_pro_controller', function(index) { + NXBT_CONTROLLER_INDEX = index; + checkForLoadInterval = setInterval(checkForLoad, 1000); +}); + +socket.on('error', function(errorMessage) { + displayError(errorMessage); +}); + +/**********************************************/ +/* Listeners and Startup Functionality */ +/**********************************************/ + +// Startup Functions +window.onload = function() { + // Run the Loader animation + setInterval(updateLoader, 85); + // // Print out the latency of setTimeout + // measureTimeoutLatency.start(120, 60); +} + +// Keydown listener +function globalKeydownHandler(evt) { + if (INPUT_DEVICE !== InputDevice.KEYBOARD) { + return + } + + evt = evt || window.event; + // Prevent scrolling on keypress + if([32, 37, 38, 39, 40].indexOf(evt.keyCode) > -1) { + evt.preventDefault(); + } + + if (Object.keys(KEYMAP).indexOf(JSON.stringify(evt.keyCode)) > -1) { + control = KEYMAP[evt.keyCode]; + if (LEFT_STICK.indexOf(control) > -1) { + INPUT_PACKET["L_STICK"][control] = true; + } else if (RIGHT_STICK.indexOf(control) > -1) { + INPUT_PACKET["R_STICK"][control] = true; + } else { + INPUT_PACKET[control] = true; + } + } +} +document.onkeydown = globalKeydownHandler; + +// Keyup listener +function globalKeyupHandler(evt) { + if (INPUT_DEVICE !== InputDevice.KEYBOARD) { + return + } + + evt = evt || window.event; + + if (Object.keys(KEYMAP).indexOf(JSON.stringify(evt.keyCode)) > -1) { + control = KEYMAP[evt.keyCode]; + if (LEFT_STICK.indexOf(control) > -1) { + INPUT_PACKET["L_STICK"][control] = false; + } else if (RIGHT_STICK.indexOf(control) > -1) { + INPUT_PACKET["R_STICK"][control] = false; + } else { + INPUT_PACKET[control] = false; + } + } +} +document.onkeyup = globalKeyupHandler; + +function disableKeyHandlers() { + document.onkeydown = false; + document.onkeyup = false; +} + +function enableKeyHandlers() { + document.onkeydown = globalKeydownHandler; + document.onkeyup = globalKeyupHandler; +} + +window.addEventListener("gamepadconnected", function(evt) { + gamepadIndex = evt.gamepad.index; + gamepadID = evt.gamepad.id; + console.log("Gamepad connected with ID:", gamepadID); + + inputs = document.getElementById("input-device"); + + input = document.createElement("option"); + input.innerHTML = gamepadID; + input.setAttribute("value", "gamepad"); + input.setAttribute("index", gamepadIndex); + input.id = gamepadID; + + inputs.appendChild(input); +}); + +window.addEventListener("gamepaddisconnected", function(evt) { + INPUT_DEVICE = InputDevice.KEYBOARD; + + HTML_CONTROLLER_MAP.classList.add('hidden'); + HTML_KEYBOARD_MAP.classList.remove('hidden'); + + CONTROLLER_INDEX = false; + gamepadInput = document.getElementById(evt.gamepad.id); + gamepadInput.remove(); +}); + +/**********************************************/ +/* UI, Input Monitoring, Gamepad Functionality */ +/**********************************************/ + +function displayOtherSessions() { + controllerIndices = Object.keys(STATE); + // If there aren't any controller sessions + if (controllerIndices.length < 1) { + HTML_CONTROLLER_SESSIONS.classList.add("hidden"); + return; + // If the only controller session is the current one + } else if (controllerIndices.length === 1 && + Number(controllerIndices[0]) === NXBT_CONTROLLER_INDEX) { + HTML_CONTROLLER_SESSIONS.classList.add("hidden"); + return; + } + + HTML_CONTROLLER_SESSIONS.classList.remove('hidden'); + HTML_CONTROLLER_SESSIONS_CONTAINER.innerHTML = "" + for (let i = 0; i < controllerIndices.length; i++) { + sessionIndex = controllerIndices[i]; + if (sessionIndex === NXBT_CONTROLLER_INDEX) { + continue + } + + let session = document.createElement("div") + session.classList.add("controller-session"); + let sessionTitle = document.createElement("h1"); + sessionTitle.innerHTML = "Session #" + sessionIndex; + let sessionEnd = document.createElement("button"); + sessionEnd.innerHTML = "END"; + sessionEnd.onclick = function() { + socket.emit('shutdown', Number(sessionIndex)); + } + + session.appendChild(sessionTitle); + session.appendChild(sessionEnd); + + HTML_CONTROLLER_SESSIONS_CONTAINER.appendChild(session); + } +} +// Set a first quick call to make sure results appear +// on page load, if available. The timeout is necessary +// due to the time it takes for the initial State to load. +setTimeout(displayOtherSessions, 250); +setInterval(displayOtherSessions, 2000); + +function displayError(errorText) { + let errorContainer = document.createElement('div'); + errorContainer.classList.add('error'); + + let errorHeader = document.createElement('h1'); + errorHeader.innerHTML = "ERROR"; + + let errorMessage = document.createElement('p'); + errorMessage.innerHTML = errorText; + + errorContainer.appendChild(errorHeader); + errorContainer.appendChild(errorMessage); + HTML_ERROR_DISPLAY.appendChild(errorContainer); + + setTimeout(function() { + errorContainer.remove(); + }, 10000); +} + +function createProController() { + HTML_CONTROLLER_SELECTION.classList.add('hidden'); + HTML_LOADER.classList.remove('hidden'); + + socket.emit('create_pro_controller'); +} + +function shutdownController() { + if (STATE[NXBT_CONTROLLER_INDEX]) { + socket.emit('shutdown', NXBT_CONTROLLER_INDEX); + } +} + +function recreateProController() { + socket.emit('create_pro_controller'); +} + +function checkForLoad() { + if (STATE[NXBT_CONTROLLER_INDEX]) { + controller_state = STATE[NXBT_CONTROLLER_INDEX].state + HTML_LOADER_TEXT.innerHTML = controller_state; + + if (controller_state === ControllerState.CONNECTED) { + // Show the connected message for 1 second + setTimeout(function() { + clearInterval(checkForLoadInterval); + HTML_LOADER.classList.add('hidden'); + HTML_CONTROLLER_CONFIG.classList.remove('hidden'); + HTML_STATUS_INDICATOR.classList.remove('hidden'); + setInterval(updateStatusIndicator, 1000); + eventLoop(); + }, 1000); + } + } +} + +function updateStatusIndicator() { + if (STATE[NXBT_CONTROLLER_INDEX]) { + controller_state = STATE[NXBT_CONTROLLER_INDEX].state; + if (controller_state === ControllerState.CONNECTED) { + changeStatusIndicatorState("indicator-green", "CONNECTED"); + } else if (controller_state === ControllerState.CONNECTING) { + changeStatusIndicatorState("indicator-yellow", "CONNECTING"); + } else if (controller_state === ControllerState.RECONNECTING) { + changeStatusIndicatorState("indicator-yellow", "RECONNECTING"); + } else if (controller_state === ControllerState.CRASHED) { + changeStatusIndicatorState("indicator-red", "CRASHED"); + } + } else { + changeStatusIndicatorState("indicator-red", "NO INPUT"); + } +} + +function changeStatusIndicatorState(className, text) { + HTML_STATUS_INDICATOR_LIGHT.className = ""; + HTML_STATUS_INDICATOR_LIGHT.classList.add(className); + HTML_STATUS_INDICATOR_TEXT.innerHTML = text; +} + +function changeInput(evt) { + inputType = evt.target.value + + if (inputType === InputDevice.KEYBOARD) { + INPUT_DEVICE = InputDevice.KEYBOARD; + + document.onkeydown = globalKeydownHandler; + document.onkeyup = globalKeyupHandler; + + HTML_CONTROLLER_MAP.classList.add('hidden'); + HTML_KEYBOARD_MAP.classList.remove('hidden'); + + CONTROLLER_INDEX = false; + } else if (inputType == InputDevice.GAMEPAD) { + INPUT_DEVICE = InputDevice.GAMEPAD; + + document.onkeydown = false; + document.onkeyup = false; + + HTML_KEYBOARD_MAP.classList.add('hidden'); + HTML_CONTROLLER_MAP.classList.remove('hidden'); + + selectedGamepad = evt.target.children[evt.target.selectedIndex] + CONTROLLER_INDEX = selectedGamepad.getAttribute("index"); + } +} + +const LOADER_ANIMATION_FRAMES = [0,1,2,3,3,2,1,0]; +let loaderFrame = 1; +let highlightedBlock = false; +function updateLoader() { + loaderBlocks = document.getElementById("loader-blocks").children; + + if (highlightedBlock === false) { + highlightedBlock = loaderBlocks[0]; + } + highlightedBlock.classList.remove("loader-block-highlight"); + highlightedBlock = loaderBlocks[LOADER_ANIMATION_FRAMES[loaderFrame]]; + highlightedBlock.classList.add("loader-block-highlight"); + + loaderFrame += 1; + if (loaderFrame >= LOADER_ANIMATION_FRAMES.length) { + loaderFrame = 0; + } +} + +function updateGamepadInput() { + let gp = navigator.getGamepads()[CONTROLLER_INDEX]; + INPUT_PACKET["L_STICK"]["X_VALUE"] = gp.axes[0] * 100; + INPUT_PACKET["L_STICK"]["Y_VALUE"] = gp.axes[1] * -100; + INPUT_PACKET["L_STICK"]["PRESSED"] = gp["buttons"][10]["pressed"]; + + INPUT_PACKET["R_STICK"]["X_VALUE"] = gp.axes[2] * 100; + INPUT_PACKET["R_STICK"]["Y_VALUE"] = gp.axes[3] * -100; + INPUT_PACKET["R_STICK"]["PRESSED"] = gp["buttons"][11]["pressed"]; + + INPUT_PACKET["DPAD_UP"] = gp["buttons"][12]["pressed"]; + INPUT_PACKET["DPAD_DOWN"] = gp["buttons"][13]["pressed"]; + INPUT_PACKET["DPAD_LEFT"] = gp["buttons"][14]["pressed"]; + INPUT_PACKET["DPAD_RIGHT"] = gp["buttons"][15]["pressed"]; + + if (gp["buttons"][16]) { + INPUT_PACKET["HOME"] = gp["buttons"][16]["pressed"]; + } + + if (gp["buttons"][17]) { + INPUT_PACKET["CAPTURE"] = gp["buttons"][17]["pressed"]; + } + + INPUT_PACKET["B"] = gp["buttons"][0]["pressed"]; + INPUT_PACKET["A"] = gp["buttons"][1]["pressed"]; + INPUT_PACKET["Y"] = gp["buttons"][2]["pressed"]; + INPUT_PACKET["X"] = gp["buttons"][3]["pressed"]; + + INPUT_PACKET["L"] = gp["buttons"][4]["pressed"]; + INPUT_PACKET["R"] = gp["buttons"][5]["pressed"]; + INPUT_PACKET["ZL"] = gp["buttons"][6]["pressed"]; + INPUT_PACKET["ZR"] = gp["buttons"][7]["pressed"]; + + INPUT_PACKET["PLUS"] = gp["buttons"][8]["pressed"]; + INPUT_PACKET["MINUS"] = gp["buttons"][9]["pressed"]; +} + +function updateGamepadDisplay() { + controls = Object.keys(INPUT_PACKET); + for (let i = 0; i < controls.length; i++) { + controlState = INPUT_PACKET[controls[i]]; + control = PRO_CONTROLLER_DISPLAY[controls[i]]; + + if (!control) { + continue; + } + + if (control.STICK) { + xRatio = (controlState["X_VALUE"] + 100)/200; + yRatio = (controlState["Y_VALUE"] + 100)/200; + xPos = control["MIN_X"] + (xRatio * control["DIFF_X"]); + yPos = control["MAX_Y"] - (yRatio * control["DIFF_Y"]); + control["ELEMENT"].style.left = xPos + "%"; + control["ELEMENT"].style.top = yPos + "%"; + } else { + if (controlState) { + control.classList.remove('hidden'); + } else { + control.classList.add('hidden'); + } + } + } +} + +let timeOld = false; +let frequency = (1/120) * 1000; +function eventLoop() { + // Update x/y ratio for the sticks based on + // pressed buttons if we're using a keyboard + if (INPUT_DEVICE == InputDevice.KEYBOARD) { + // Calculating left x/y stick values + lXValue = 0 + lYValue = 0 + if (INPUT_PACKET["L_STICK"]["LS_LEFT"]) { + lXValue -= 100 + } + if (INPUT_PACKET["L_STICK"]["LS_RIGHT"]) { + lXValue += 100 + } + if (INPUT_PACKET["L_STICK"]["LS_UP"]) { + lYValue += 100 + } + if (INPUT_PACKET["L_STICK"]["LS_DOWN"]) { + lYValue -= 100 + } + INPUT_PACKET["L_STICK"]["X_VALUE"] = lXValue + INPUT_PACKET["L_STICK"]["Y_VALUE"] = lYValue + + // Calculating left x/y stick values + rXValue = 0 + rYValue = 0 + if (INPUT_PACKET["R_STICK"]["RS_LEFT"]) { + rXValue -= 100 + } + if (INPUT_PACKET["R_STICK"]["RS_RIGHT"]) { + rXValue += 100 + } + if (INPUT_PACKET["R_STICK"]["RS_UP"]) { + rYValue += 100 + } + if (INPUT_PACKET["R_STICK"]["RS_DOWN"]) { + rYValue -= 100 + } + INPUT_PACKET["R_STICK"]["X_VALUE"] = rXValue + INPUT_PACKET["R_STICK"]["Y_VALUE"] = rYValue + } else if (INPUT_DEVICE == InputDevice.GAMEPAD) { + updateGamepadInput(); + } + + // Only send packet if it's not a duplicate of previous. + // We can do this since NXBT will hold the previously sent value + // until we send it a new one. + if (JSON.stringify(INPUT_PACKET) !== JSON.stringify(INPUT_PACKET_OLD)) { + socket.emit('input', JSON.stringify([NXBT_CONTROLLER_INDEX, INPUT_PACKET])); + INPUT_PACKET_OLD = JSON.parse(JSON.stringify(INPUT_PACKET)); + } + + updateGamepadDisplay() + + // if (!timeOld) { + // timeOld = performance.now(); + // } + // timeNew = performance.now(); + // delta = timeNew - timeOld; + // diff = delta - frequency; + + // if (diff > 0) { + // setTimeout(eventLoop, frequency - diff); + // } else { + // setTimeout(eventLoop, frequency); + // } + // timeOld = timeNew; + + requestAnimationFrame(eventLoop); +} + +function sendMacro() { + let macro = HTML_MACRO_TEXT.value.toUpperCase(); + socket.emit('macro', JSON.stringify([NXBT_CONTROLLER_INDEX, macro])); +} + +/**********************************************/ +/* Debug Functionality */ +/**********************************************/ + +const measureTimeoutLatency = { + debug: false, + count: 0, + maxCount: 0, + deltaSum: 0, + diffSum: 0, + timeOld: false, + rawFrequency: false, + frequency: false, + + start: function(frequency, iterations) { + this.rawFrequency = frequency; + this.frequency = (1/frequency)*1000; + this.maxCount = iterations; + this.timeOld = performance.now(); + this.loop(this); + }, + + loop: function(context) { + timeNew = performance.now(); + delta = timeNew - context.timeOld; + diff = delta - context.frequency; + + if (context.debug) { + console.log(diff); + } + + context.deltaSum += Math.abs(delta); + context.diffSum += Math.abs(diff); + context.count += 1 + + if (context.count >= context.maxCount) { + avgDelta = context.deltaSum/context.count; + avgDiff = context.diffSum/context.count; + + console.log("Average Time setTimeout Delta (ms):", avgDelta); + console.log("Average Difference between Delta and Frequency (ms):", avgDiff); + } else { + if (diff > 0) { + setTimeout(function(){context.loop(context);}, context.frequency - diff); + } else { + setTimeout(function(){context.loop(context);}, context.frequency); + } + context.timeOld = timeNew; + } + } +} diff --git a/nxbt/web/static/procon-transparent.svg b/nxbt/web/static/procon-transparent.svg new file mode 100644 index 0000000..110ba9c --- /dev/null +++ b/nxbt/web/static/procon-transparent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nxbt/web/static/procon.svg b/nxbt/web/static/procon.svg new file mode 100644 index 0000000..1e10973 --- /dev/null +++ b/nxbt/web/static/procon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nxbt/web/templates/index.html b/nxbt/web/templates/index.html index f8fa295..4e3fe10 100644 --- a/nxbt/web/templates/index.html +++ b/nxbt/web/templates/index.html @@ -1,69 +1,214 @@ - NXBT: Control Your Switch Locally or Remotely + NXBT Webapp + + + + +
+
+
+

NXBT

v0.1
+
+ +
+
+ +
+
+

Create a Controller

+ +
+
+ +
+ Pro Controller +
+ +
+ + + + + + + +
+ + +
+ - + \ No newline at end of file