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 0000000..988e2c1 Binary files /dev/null and b/nxbt/web/static/favicon.png differ 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