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
+
+
+
+
+
+
+
+
+ Create a Controller
+
+
+
+
+ Joy-Con (L)
+
+
+
+
+
+ Pro Controller
+
+
+
+
+
+ Joy-Con (R)
+
+
+
+
+
+ Loading
+
+
Tips
+
1. First Time Connection
+
+ If this is your first time connecting to your Nintendo
+ Switch with NXBT, please ensure that you're on the "Change Grip/Order" menu.
+ This sub-menu is available from within the Controllers menu, located on the
+ bottom row of the Switch's Home Screen.
+
+
2. Reconnection
+
+ If you've previously connected to this Nintendo Switch using NXBT,
+ please ensure that you're on the Home Screen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Controller Commands
+
Shutdown Controller
+
Recreate Controller
+
+
Controller Macro
+
+ Note: the main input device will override a running
+ macro.
+
+
+
Run Macro
+
+
+
+
+ Other Controller Sessions
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file