Upload files to "Shell_Transmitter"
This commit is contained in:
631
Shell_Transmitter/code.py
Normal file
631
Shell_Transmitter/code.py
Normal 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()
|
||||||
10
Shell_Transmitter/controller_config.py
Normal file
10
Shell_Transmitter/controller_config.py
Normal 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"
|
||||||
37
Shell_Transmitter/controller_helpers.py
Normal file
37
Shell_Transmitter/controller_helpers.py
Normal 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("---------------------------------")
|
||||||
41
Shell_Transmitter/help.txt
Normal file
41
Shell_Transmitter/help.txt
Normal 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
|
||||||
Reference in New Issue
Block a user