Upload files to "Shell_Transmitter"

This commit is contained in:
2025-11-13 21:48:14 +00:00
parent 6b53d39fff
commit ded5b958fc
4 changed files with 719 additions and 0 deletions

631
Shell_Transmitter/code.py Normal file
View File

@@ -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 <name_or_mac> 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 <filename>)")
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 <ms> (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()

View File

@@ -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"

View File

@@ -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("---------------------------------")

View File

@@ -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 <name/mac> - Pairs a specific device by its name or MAC address.
//unpair all - Unpairs all devices. They will not auto-reconnect.
//unpair /d <name/mac> - 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 <text> - Asks for input with a custom prompt (e.g., //prompt /p Password:)
//script <file.txt> - Runs a script file from the controller's storage.
//delay <ms> - Pauses the receiver for <ms> milliseconds. (e.g. //delay 1000)
[Keyboard Commands]
//hold <key> [key...] - Holds one or more keys.
- (e.g., //hold //l_ctrl A B) holds L_CTRL, A, and B.
//release <key> [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 <val> - Moves mouse relative X (e.g., //relx -50)
//rely <val> - Moves mouse relative Y (e.g., //rely 25)
//scroll <val> - Scrolls the mouse wheel (e.g., //scroll -1)
//l_click, //r_click, //m_click, //back_click, //fwd_click