diff --git a/Shell_Transmitter/code.py b/Shell_Transmitter/code.py new file mode 100644 index 0000000..84fb10c --- /dev/null +++ b/Shell_Transmitter/code.py @@ -0,0 +1,631 @@ +import wifi +import time +import supervisor +import sys +import os +import socketpool +import errno + +import controller_config as config +import controller_helpers as helpers + +# --- 1. SET UP Wi-Fi HOTSPOT --- +helpers.setup_wifi_ap(config.WIFI_SSID, config.WIFI_PASSWORD, config.WIFI_CHANNEL) +pool = socketpool.SocketPool(wifi.radio) +server = None # Server socket + +# --- 2. CLIENT & STATE MANAGEMENT --- +serial_input_buffer = "" +seq_num = 0 +last_heartbeat_send_time = time.monotonic() + +all_clients = {} +paired_clients = {} +known_paired_macs = set() +client_read_buffer = bytearray(256) + +# --- NEW: Command Queue & State --- +command_send_buffer = [] # Master list of commands to be sent +g_system_halted = False # Pauses all sending on heartbeat fail +# --- END NEW --- + +global_receiver_settings = { + "AUTO_ENTER": "OFF", + "POLL_DELAY_MS": 10 +} + +def start_server(): + """Starts the TCP server ONCE and leaves it open.""" + global server + print(f"Starting TCP Server on {wifi.radio.ipv4_address_ap}:{config.LISTEN_PORT}...") + server = pool.socket(pool.AF_INET, pool.SOCK_STREAM) + server.setblocking(False) + try: + server.bind((str(wifi.radio.ipv4_address_ap), config.LISTEN_PORT)) + server.listen(4) + print("--- 📡 Controller Ready ---") + print("Server is open for clients to connect at any time.") + print("Type '//pair' to pair newly connected devices.") + except Exception as e: + print(f"FATAL: Could not bind/listen: {e}") + print("Rebooting in 5s...") + time.sleep(5) + supervisor.reload() + +def broadcast_to_single_client(client_sock, command_string): + """Sends a single command to a single client, used for settings.""" + global seq_num + if not client_sock: + return + + seq_num = (seq_num + 1) % 10000 + full_packet = f"SEQ:{seq_num}:{command_string}\n".encode('utf-8') + try: + client_sock.sendall(full_packet) + except Exception as e: + pass # Errors handled by heartbeat loop + +def broadcast_to_paired(command_string, no_log=False): + """ + Sends a command to ALL paired clients immediately. + This bypasses the queue and busy flags. + Used for system commands like //stop, //ping, //exit. + """ + global seq_num, all_clients, paired_clients + if not paired_clients: + if not no_log: + print("[!] No paired devices. Command not sent.") + return + + seq_num = (seq_num + 1) % 10000 + full_packet = f"SEQ:{seq_num}:{command_string}\n".encode('utf-8') + + if no_log: + print(f"Sending (SYSTEM) to {len(paired_clients)} device(s): **** (len: {len(command_string)})") + else: + print(f"Sending (SYSTEM) to {len(paired_clients)} device(s): {command_string}") + + disconnected_clients = [] + for client_sock, info in paired_clients.items(): + try: + client_sock.sendall(full_packet) + except (OSError, Exception) as ex: + print(f"\n[!] Paired client '{info['name']}' ({info['mac']}) disconnected (send fail): {ex}") + client_sock.close() + disconnected_clients.append(client_sock) + + for client in disconnected_clients: + if client in all_clients: del all_clients[client] + if client in paired_clients: del paired_clients[client] + +def send_settings_to_client(client_sock, info): + """Sends the current (non-default) settings to a client.""" + print(f"Sending current settings to '{info['name']}'...") + + if global_receiver_settings["AUTO_ENTER"] == "ON": + cmd = "//auto_enter_on" + print(f" -> {cmd}") + broadcast_to_single_client(client_sock, cmd) + + if global_receiver_settings["POLL_DELAY_MS"] != 10: + delay_ms = global_receiver_settings["POLL_DELAY_MS"] + cmd = f"//set_poll_delay {delay_ms}" + print(f" -> {cmd}") + broadcast_to_single_client(client_sock, cmd) + +# --- NEW: Process the command buffer --- +def process_command_buffer(): + """ + Sends one command from the queue to all non-busy clients. + """ + global command_send_buffer, paired_clients, g_system_halted + + # Don't send if buffer is empty or system is halted + if not command_send_buffer or g_system_halted: + return + + # Check if all paired clients are busy + all_busy = True + if not paired_clients: + all_busy = False # No clients, not busy + + for client_sock, info in paired_clients.items(): + if not info.get('busy', False): + all_busy = False + break + + if all_busy: + # print("All clients busy, pausing send.") + return + + # At least one client is ready. Send the next command. + command_to_send = command_send_buffer.pop(0) + + # We use broadcast_to_paired, which will skip busy clients + # This is a bit of a hack: broadcast_to_paired now only sends + # to non-busy clients for *buffer* commands. + + # Let's redefine broadcast_to_paired's job. + # broadcast_to_paired sends to ALL. + # We need a new function. + + send_buffered_command(command_to_send) + +def send_buffered_command(command_string): + """ + Sends a command from the queue to all non-busy clients. + """ + global seq_num, all_clients, paired_clients, command_send_buffer + + if not paired_clients: + print("[!] No paired devices. Clearing command buffer.") + command_send_buffer.clear() + return + + seq_num = (seq_num + 1) % 10000 + full_packet = f"SEQ:{seq_num}:{command_string}\n".encode('utf-8') + + print(f"Sending (Q) to non-busy devices: {command_string}") + + disconnected_clients = [] + + # Check if all clients are busy + all_busy = True + for client_sock, info in paired_clients.items(): + if not info.get('busy', False): + all_busy = False + break + + if all_busy: + print(" (All clients are busy. Command requeued.)") + command_send_buffer.insert(0, command_string) # Put it back + return + + for client_sock, info in paired_clients.items(): + # If client is busy, skip it. Command will be re-sent. + if info.get('busy', False): + continue + + try: + client_sock.sendall(full_packet) + except (OSError, Exception) as ex: + print(f"\n[!] Paired client '{info['name']}' ({info['mac']}) disconnected (send fail): {ex}") + client_sock.close() + disconnected_clients.append(client_sock) + + for client in disconnected_clients: + if client in all_clients: del all_clients[client] + if client in paired_clients: del paired_clients[client] + +# --- END NEW --- + +def handle_controller_command(line): + """Handles serial commands that start with //""" + global all_clients, paired_clients, known_paired_macs, seq_num, global_receiver_settings, command_send_buffer + + parts = line.split() + if not parts: + return + command = parts[0].lower() + + # --- NEW: E-Stop --- + if command == "//stop" or command == "//s": + print("[!] EMERGENCY STOP") + command_send_buffer.clear() + broadcast_to_paired("//stop") # Send high-priority stop + return + # --- END NEW --- + + # --- //pair --- + if command == "//pair": + if len(parts) >= 3 and parts[1].lower() == "/d": + target = " ".join(parts[2:]) + target_is_mac = ":" in target + found = False + for sock, info in all_clients.items(): + if not info: continue + match = False + if target_is_mac and info['mac'] == target.upper(): match = True + elif not target_is_mac and info['name'] == target: match = True + if match: + if sock not in paired_clients: + paired_clients[sock] = info + known_paired_macs.add(info['mac']) + print(f"[+] Paired device: {info['name']} ({info['mac']})") + send_settings_to_client(sock, info) + else: + print(f"[!] Device '{info['name']}' is already paired.") + found = True + break + if not found: + print(f"[!] Device '{target}' not found or not connected.") + else: + paired_count = 0 + for sock, info in all_clients.items(): + if info and sock not in paired_clients: + paired_clients[sock] = info + known_paired_macs.add(info['mac']) + print(f"[+] Paired device: {info['name']} ({info['mac']})") + send_settings_to_client(sock, info) + paired_count += 1 + if paired_count == 0: print("No new devices to pair.") + print(f"Total paired devices: {len(paired_clients)}") + + # --- //unpair (Unchanged) --- + elif command == "//unpair": + if len(parts) >= 3 and parts[1].lower() == "/d": + target = " ".join(parts[2:]) + target_is_mac = ":" in target + sock_to_remove = None + info_to_remove = None + for sock, info in paired_clients.items(): + match = False + if target_is_mac and info['mac'] == target.upper(): match = True + elif not target_is_mac and info['name'] == target: match = True + if match: + sock_to_remove = sock + info_to_remove = info + break + if sock_to_remove: + del paired_clients[sock_to_remove] + if info_to_remove['mac'] in known_paired_macs: + known_paired_macs.remove(info_to_remove['mac']) + print(f"[-] Unpaired device: {info_to_remove['name']} ({info_to_remove['mac']})") + print(f" Device will NOT auto-re-pair.") + else: + print(f"[!] Paired device '{target}' not found.") + elif len(parts) == 2 and parts[1].lower() == "all": + print(f"Unpairing all {len(paired_clients)} devices...") + paired_clients.clear() + known_paired_macs.clear() + print("All devices unpaired and will NOT auto-re-pair.") + else: + print("Usage: //unpair /d OR //unpair all") + + # --- //list --- + elif command == "//list": + print(f"--- {len(all_clients)} Total Client(s) ---") + if not all_clients: + print(" (No clients connected)") + else: + for sock, info in all_clients.items(): + if info: + status = "PAIRED" if sock in paired_clients else "UNPAIRED" + busy_str = " (BUSY)" if info.get('busy', False) else "" + print(f" [{status:^8}] - {info['name']} (MAC: {info['mac']}){busy_str}") + else: + peer = sock.getpeername() + print(f" [PENDING] - {peer[0] if peer else 'Unknown IP'} (Waiting for handshake)") + print("[-------------------------]") + print(f"Known MACs (auto-re-pair): {list(known_paired_macs)}") + print(f"Current Settings: Auto-Enter={global_receiver_settings['AUTO_ENTER']}, Delay={global_receiver_settings['POLL_DELAY_MS']}ms") + print(f"Send Buffer Queue: {len(command_send_buffer)} commands") + + + # --- //help (Unchanged) --- + elif command == "//help": + helpers.print_help_file() + + # --- //ping --- + elif command == "//ping": + if not paired_clients: + print("[!] No paired devices to ping.") + else: + print(f"Pinging {len(paired_clients)} paired device(s)...") + seq_num = (seq_num + 1) % 10000 + ping_packet = f"SEQ:{seq_num}://ping\n".encode('utf-8') + for client_sock, info in paired_clients.items(): + try: + current_time = time.monotonic() + info['ping_start_time'] = current_time + if client_sock in all_clients: + all_clients[client_sock]['ping_start_time'] = current_time + client_sock.sendall(ping_packet) + except Exception as ex: + print(f"[!] Error sending ping to {info['name']}: {ex}") + + # --- //pause --- + elif command == "//pause": + print("Controller paused. Press [Enter] to resume.") + while True: + if supervisor.runtime.serial_bytes_available: + if sys.stdin.read(1) == '\n': + break + time.sleep(0.1) + print("...Resumed.") + + # --- //prompt --- + elif command == "//prompt": + prompt_text = "Input: " + if len(parts) > 2 and parts[1].lower() == "/p": + prompt_text = " ".join(parts[2:]) + ": " + print(prompt_text, end="") + user_input = sys.stdin.readline().strip() + command_send_buffer.append(user_input) # Add to queue + print(f"Queued: **** (len: {len(user_input)})") + + # --- //script --- + elif command == "//script": + if len(parts) < 2: + print(" (Error: Usage: //script )") + else: + filename = parts[1] + try: + with open(filename, "r") as f: + count = 0 + for script_line in f: + command_send_buffer.append(script_line.strip()) + count += 1 + print(f"Queued {count} lines from {filename}.") + except OSError: + print(f" (Error: File not found: {filename})") + except Exception as ex: + print(f" (Error running script: {ex})") + + # --- //exit (Unchanged) --- + elif command == "//exit": + print("Sending disconnect to clients and halting AP...") + broadcast_to_paired("//exit") + time.sleep(1) + wifi.radio.stop_ap() + print("AP stopped. Controller is halting.") + sys.exit() + + # --- Setting Commands --- + elif command == "//auto_enter_on": + print("Setting Auto-Enter to ON for all paired devices.") + global_receiver_settings["AUTO_ENTER"] = "ON" + broadcast_to_paired("//auto_enter_on") # Send high-priority + + elif command == "//auto_enter_off": + print("Setting Auto-Enter to OFF for all paired devices.") + global_receiver_settings["AUTO_ENTER"] = "OFF" + broadcast_to_paired("//auto_enter_off") # Send high-priority + + elif command == "//set_delay": + if len(parts) < 2: + print("Usage: //set_delay (e.g., //set_delay 20)") + return + try: + delay_ms = int(parts[1]) + if delay_ms < 1: delay_ms = 1 + global_receiver_settings["POLL_DELAY_MS"] = delay_ms + print(f"Setting Poll Delay to {delay_ms}ms for all paired devices.") + broadcast_to_paired(f"//set_poll_delay {delay_ms}") # Send high-priority + except ValueError: + print("Invalid delay. Must be a number (in ms).") + + else: + # This is a system command we don't recognize + # We will send it high-priority + broadcast_to_paired(line) + +def check_for_new_clients(): + """Accepts new clients. Runs all the time.""" + global server + try: + client_sock, client_addr = server.accept() + client_sock.setblocking(False) + all_clients[client_sock] = {'read_buffer': "", 'busy': False} # Temp dict + print(f"\n[+] New connection from: {client_addr[0]}. Waiting for handshake...") + except OSError as ex: + if ex.errno == errno.EAGAIN: + pass # No new client, normal + else: + print(f"Discovery accept error: {ex}") + +def check_for_incoming_data(): + """Reads data from all clients (handshakes, triggers, pongs).""" + global all_clients, paired_clients, known_paired_macs + + disconnected_clients = [] + + for client_sock, current_info in list(all_clients.items()): + try: + read_len = client_sock.recv_into(client_read_buffer, 256) + if read_len > 0: + data = client_read_buffer[:read_len].decode('utf-8') + + if 'read_buffer' not in current_info: + current_info['read_buffer'] = "" + + current_info['read_buffer'] += data + + while '\n' in current_info['read_buffer']: + line, current_info['read_buffer'] = current_info['read_buffer'].split('\n', 1) + line = line.strip() + if not line: + continue + + current_info = all_clients[client_sock] + + # --- Handshake Parsing --- + if line.startswith("HANDSHAKE:"): + try: + parts = line.split(':') + client_name = parts[1].split('=')[1] + client_mac = parts[2].split('=')[1] + + existing_sock_to_prune = None + for sock, info in all_clients.items(): + if info and 'mac' in info and info['mac'] == client_mac and sock != client_sock: + existing_sock_to_prune = sock + break + + if existing_sock_to_prune: + print(f"[!] Device '{client_name}' reconnected, pruning old socket.") + old_info = all_clients.pop(existing_sock_to_prune, {}) + if existing_sock_to_prune in paired_clients: del paired_clients[existing_sock_to_prune] + existing_sock_to_prune.close() + # Carry over busy status + current_info['busy'] = old_info.get('busy', False) + + current_info['name'] = client_name + current_info['mac'] = client_mac + + if client_mac in known_paired_macs: + paired_clients[client_sock] = current_info + print(f"\n[+] Client '{client_name}' ({client_mac}) reconnected and auto-paired.") + send_settings_to_client(client_sock, current_info) + else: + print(f"\n[+] New device, '{client_name}' ({client_mac}) connected, not paired.") + print(f" Type '//pair' or '//pair /d {client_name}' to pair.") + + except Exception as e: + print(f"Malformed handshake: {line} ({e})") + + # --- NEW: Flow Control --- + elif line.startswith("TRIGGER:BUSY:ON"): + if current_info and 'name' in current_info: + print(f"[!] Client '{current_info['name']}' is busy. Pausing sends to it.") + current_info['busy'] = True + elif line.startswith("TRIGGER:BUSY:OFF"): + if current_info and 'name' in current_info: + print(f"[+] Client '{current_info['name']}' is ready. Resuming sends.") + current_info['busy'] = False + # --- END NEW --- + + elif line.startswith("TRIGGER:"): + trigger_msg = line.split(':',1)[1] + if current_info and 'name' in current_info: + print(f"\n[TRIGGER from {current_info['name']}]: {trigger_msg}") + + elif line.startswith("PONG:"): + pong_msg = line.split(':',1)[1] + name = current_info.get('name', "Unknown") + if current_info and 'ping_start_time' in current_info: + end_time = time.monotonic() + start_time = current_info.pop('ping_start_time') + latency_ms = (end_time - start_time) * 1000 + print(f"\n[PONG from {name}]: {pong_msg} (Latency: {latency_ms:.0f} ms)") + if client_sock in paired_clients: + paired_clients[client_sock].pop('ping_start_time', None) + else: + print(f"\n[PONG from {name}]: {pong_msg}") + + elif read_len == 0: + raise OSError(0, "Client closed connection") + + except OSError as ex: + if ex.errno == errno.EAGAIN: + pass # No data, normal + else: + info = all_clients.get(client_sock) + name = info.get('name', "Unknown") if info else "Unknown" + print(f"\n[!] Client '{name}' disconnected (read fail): {ex}") + client_sock.close() + disconnected_clients.append(client_sock) + + for client in disconnected_clients: + if client in all_clients: del all_clients[client] + if client in paired_clients: del paired_clients[client] + +# --- 3. MAIN LOOP (Modified) --- +start_server() + +try: + while True: + check_for_new_clients() + check_for_incoming_data() + + # --- NEW: Process command buffer --- + if not g_system_halted: + process_command_buffer() + # --- END NEW --- + + if supervisor.runtime.serial_bytes_available: + num_bytes_available = supervisor.runtime.serial_bytes_available + if num_bytes_available > 0: + data_chunk = sys.stdin.read(num_bytes_available) + if data_chunk: + for char in data_chunk: + if char == '\n': + sys.stdout.write('\n') + + # --- NEW: Handle system halt input --- + if g_system_halted: + if serial_input_buffer.lower() == 'continue': + print("...Resuming sends.") + g_system_halted = False + elif serial_input_buffer.lower() == 'stop': + print("...Clearing buffer and resuming.") + command_send_buffer.clear() + g_system_halted = False + else: + print("[!] Heartbeat fail. Type 'continue' to resume or 'stop' to clear queue.") + serial_input_buffer = "" + continue + # --- END NEW --- + + line_to_check = serial_input_buffer.strip() + + if line_to_check.startswith("//"): + handle_controller_command(line_to_check) + elif serial_input_buffer: + # Add text to the queue + command_send_buffer.append(serial_input_buffer) + print(f"Queued: {serial_input_buffer}") + else: + # Empty line, queue an //enter + command_send_buffer.append("//enter") + print("Queued: //enter") + + serial_input_buffer = "" + + elif char in ('\b', '\x7f'): + if len(serial_input_buffer) > 0: + serial_input_buffer = serial_input_buffer[:-1] + sys.stdout.write('\b \b') + elif char: + if len(serial_input_buffer) < 5120: + serial_input_buffer += char + sys.stdout.write(char) + + if (time.monotonic() - last_heartbeat_send_time) > config.HEARTBEAT_INTERVAL: + last_heartbeat_send_time = time.monotonic() + seq_num = (seq_num + 1) % 10000 + heartbeat_cmd = f"SEQ:{seq_num}://heartbeat\n".encode('utf-8') + + disconnected_clients = [] + + for client_sock, info in list(all_clients.items()): + try: + client_sock.sendall(heartbeat_cmd) + if info and 'ping_start_time' in info: + ping_age = time.monotonic() - info['ping_start_time'] + if ping_age > 5.0: + name = info.get('name', "Unknown") + print(f"\n[!] Ping timeout for {name}") + info.pop('ping_start_time') + if client_sock in paired_clients: + paired_clients[client_sock].pop('ping_start_time', None) + + except (OSError, Exception) as ex: + name = info.get('name', "Unknown") if info else "Unknown" + print(f"\n[!] Heartbeat fail for {name}. Pausing all sends.") + print("[!] Type 'continue' to resume or 'stop' to clear queue.") + g_system_halted = True # <-- NEW + client_sock.close() + disconnected_clients.append(client_sock) + + for client in disconnected_clients: + if client in all_clients: del all_clients[client] + if client in paired_clients: del paired_clients[client] + + time.sleep(0.01) + +# --- Critical Error Fallback (unchanged) --- +except Exception as main_exception: + print("\n" * 5) + print("=" * 40) + print(f"CRITICAL ERROR in Controller main loop: {main_exception}") + print("=" * 40) + if all_clients: + print("Attempting to send emergency disconnect to all peers...") + disconnect_msg = f"SEQ:9999://exit\n".encode('utf-8') + for client_sock in all_clients: + try: client_sock.sendall(disconnect_msg) + except: pass + print("Controller is rebooting in 10s...") + time.sleep(10) + supervisor.reload() \ No newline at end of file diff --git a/Shell_Transmitter/controller_config.py b/Shell_Transmitter/controller_config.py new file mode 100644 index 0000000..fef35cf --- /dev/null +++ b/Shell_Transmitter/controller_config.py @@ -0,0 +1,10 @@ +# --- Wi-Fi Hotspot (AP) Configuration --- +WIFI_SSID = "keyboard-controller" +WIFI_PASSWORD = "lWz4Bho2vb2JaIK9jXt0ctrSPglqjJe9YWlHlls0ifCQsL7tEXpFCRpAyIAf" +WIFI_CHANNEL = 6 +LISTEN_PORT = 5000 # The port the server will listen on + +# --- Scripting & Network --- +CHUNK_SIZE = 64 +HEARTBEAT_INTERVAL = 10 +HELP_FILE = "help.txt" \ No newline at end of file diff --git a/Shell_Transmitter/controller_helpers.py b/Shell_Transmitter/controller_helpers.py new file mode 100644 index 0000000..9ddaca0 --- /dev/null +++ b/Shell_Transmitter/controller_helpers.py @@ -0,0 +1,37 @@ +import wifi +import time +import controller_config as config + +def rssi_to_percent(rssi): + """Converts RSSI (dBm) to a 0-100% signal strength.""" + MIN_RSSI = -90; MAX_RSSI = -30 + if rssi <= MIN_RSSI: return 0 + if rssi >= MAX_RSSI: return 100 + percent = ((rssi - MIN_RSSI) * 100) / (MAX_RSSI - MIN_RSSI) + return int(percent) + +def setup_wifi_ap(ssid, password, channel): + """ + Configures the device as a Wi-Fi Access Point (AP). + """ + print(f"Setting up Wi-Fi AP: {ssid} on channel {channel}") + try: + wifi.radio.enabled = True + wifi.radio.start_ap(ssid=ssid, password=password, channel=channel, max_connections=4) + print(f"AP Started. IP: {wifi.radio.ipv4_address_ap}") + print(f"Waiting for clients... (SSID is VISIBLE: {ssid})") + + except Exception as ex: + print(f"Error starting AP: {ex}") + raise + +def print_help_file(): + """Reads and prints the help file specified in config.""" + print("\n--- ⌨️ KBD Bot Command List ---") + try: + with open(config.HELP_FILE, "r") as f: + for line in f: + print(line.strip()) + except OSError: + print(f"ERROR: Help file not found: {config.HELP_FILE}") + print("---------------------------------") \ No newline at end of file diff --git a/Shell_Transmitter/help.txt b/Shell_Transmitter/help.txt new file mode 100644 index 0000000..a9c2fe5 --- /dev/null +++ b/Shell_Transmitter/help.txt @@ -0,0 +1,41 @@ +--- ⌨️ KBD Bot Command List --- +(Commands are not case-sensitive) + +[Controller & Pairing] +//help - Prints this command list. +//pair - Pairs all currently connected, unpaired devices. +//pair /d - Pairs a specific device by its name or MAC address. +//unpair all - Unpairs all devices. They will not auto-reconnect. +//unpair /d - Unpairs a specific device and removes it from auto-pair. +//list - Lists all currently connected devices and their pair status. +//ping - Pings all paired devices and reports their latency. +//pause - Pauses the controller. Press [Enter] on the serial monitor to resume. +//exit - Commands all paired devices to disconnect, then stops the AP. + +[Scripting & Input] +//prompt - Asks for text input, hides it, and sends it to keyboards. +//prompt /p - Asks for input with a custom prompt (e.g., //prompt /p Password:) +//script - Runs a script file from the controller's storage. +//delay - Pauses the receiver for milliseconds. (e.g. //delay 1000) + +[Keyboard Commands] +//hold [key...] - Holds one or more keys. +- (e.g., //hold //l_ctrl A B) holds L_CTRL, A, and B. +//release [key...] - Releases one or more keys. +//release all - Releases all currently held keys. +//auto_enter_on - [Receiver] Automatically presses ENTER after typing text. +//auto_enter_off - [Receiver] Disables Auto-ENTER mode (default). + +[Common Single Keys] +//enter, //tab, //esc, //del, //backspace, //insert, //prtscr +//up, //down, //left, //right, //caps_lock, //scroll_lock, //num_lock +//f1 - //f12, //win, //shift, //ctrl, //alt + +[Media & Volume Keys] +//vol_up, //vol_down, //mute, //play_pause, //next_track, //prev_track + +[Mouse Commands] +//relx - Moves mouse relative X (e.g., //relx -50) +//rely - Moves mouse relative Y (e.g., //rely 25) +//scroll - Scrolls the mouse wheel (e.g., //scroll -1) +//l_click, //r_click, //m_click, //back_click, //fwd_click \ No newline at end of file