diff --git a/pc_companion/README.md b/pc_companion/README.md new file mode 100644 index 0000000..d6819a7 --- /dev/null +++ b/pc_companion/README.md @@ -0,0 +1,18 @@ +# RemoteMapper PC Companion (本地脚本扩展守护程序) + +通过在 ESP32 WebUI 中将遥控器按键映射为 F13 ~ F24 无冲突按键,本程序可在电脑后台以 0.0% CPU 占用 实时监听并异步执行本地自定义脚本。 + +## 快速使用方法 + +### 第一步:在 WebUI 中将遥控器按键配置为 F13 ~ F24 +1. 电脑浏览器打开 WebUI (http://192.168.2.179 或 http://192.168.4.1); +2. 点击想绑定的遥控器按键(例如 菜单键、电视键、电源键 或任意按键); +3. 在弹出的高级下拉菜单中,选择 ??? 本地自定义脚本专用键 (F13 ~ F24 无冲突)(例如 F13 或 F14 等); +4. 点击 保存映射。 + +### 第二步:配置你想执行的本地动作 +打开 pc_companion/config.json,根据需求修改或添加命令: +随时修改保存 config.json,程序会自动热更新重新载入! + +### 第三步:启动守护进程 +双击运行 start_companion.bat 即可! diff --git a/pc_companion/autostart_manager.py b/pc_companion/autostart_manager.py new file mode 100644 index 0000000..b5d7e03 --- /dev/null +++ b/pc_companion/autostart_manager.py @@ -0,0 +1,56 @@ +锘�# -*- coding: utf-8 -*- +import os +import sys +import winreg +import subprocess + +REG_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run" +APP_NAME = "RemoteMapperCompanion" + +def get_pythonw_path(): + python_exe = sys.executable + pythonw = os.path.join(os.path.dirname(python_exe), "pythonw.exe") + if os.path.exists(pythonw): + return pythonw + return python_exe + +def enable_autostart(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + host_script = os.path.join(script_dir, "remotemapper_host.py") + pythonw = get_pythonw_path() + + cmd = f'"{pythonw}" "{host_script}"' + + # Write to HKCU Run + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_SET_VALUE) + winreg.SetValueEx(key, APP_NAME, 0, winreg.REG_SZ, cmd) + winreg.CloseKey(key) + + print("=" * 60) + print("[OK] 鎴愬姛璁剧疆寮�鏈鸿嚜鍚姩锛�") + print(f" 鑷惎鐩爣: {cmd}") + print(" 鍚姩妯″紡: 鍚庡彴闈欓粯鏃犲脊绐楄繍琛� (pythonw.exe)") + print("=" * 60) + + # Launch immediately in background + subprocess.Popen([pythonw, host_script], cwd=script_dir) + print("[OK] 宸插湪鍚庡彴涓烘偍鍗冲埢鍚姩闈欓粯瀹堟姢杩涚▼锛�") + +def disable_autostart(): + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_SET_VALUE) + winreg.DeleteValue(key, APP_NAME) + winreg.CloseKey(key) + print("=" * 60) + print("[OK] 宸叉垚鍔熶粠寮�鏈鸿嚜鍚姩涓Щ闄わ紒") + print("=" * 60) + except FileNotFoundError: + print("[INFO] 褰撳墠鏈紑鍚紑鏈鸿嚜鍚��") + except Exception as e: + print(f"[ERROR] 绉婚櫎澶辫触: {e}") + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1].lower() == "disable": + disable_autostart() + else: + enable_autostart() diff --git a/pc_companion/build_exe.bat b/pc_companion/build_exe.bat new file mode 100644 index 0000000..825d939 --- /dev/null +++ b/pc_companion/build_exe.bat @@ -0,0 +1,9 @@ +@echo off +cd /d "%~dp0" +echo 正在安装/检查 PyInstaller... +python -m pip install pyinstaller +echo 正在打包为无黑框独立 exe... +pyinstaller --noconsole --onefile --name RemoteMapperHost remotemapper_host.py +echo. +echo [OK] 打包完成!独立免安装程序位于: dist\RemoteMapperHost.exe +pause diff --git a/pc_companion/config.json b/pc_companion/config.json new file mode 100644 index 0000000..0bc7b89 --- /dev/null +++ b/pc_companion/config.json @@ -0,0 +1,57 @@ +{ + "F13": { + "name": "鎵撳紑璁$畻鍣�", + "command": "calc.exe", + "enabled": true + }, + "F14": { + "name": "鎵撳紑娴忚鍣ㄧ綉鍧�", + "command": "explorer https://gemini.google.com", + "enabled": true + }, + "F15": { + "name": "鎵撳紑璁颁簨鏈�", + "command": "notepad.exe", + "enabled": true + }, + "F16": { + "name": "杩愯鑷畾涔� Python 浠诲姟", + "command": "python scripts/demo_task.py", + "enabled": true + }, + "F17": { + "name": "杩愯鑷畾涔夋壒澶勭悊", + "command": "scripts/demo_action.bat", + "enabled": true + }, + "F19": { + "name": "鑷畾涔夋Ы浣� 7", + "command": "notepad.exe", + "enabled": false + }, + "F20": { + "name": "鑷畾涔夋Ы浣� 8", + "command": "", + "enabled": false + }, + "F21": { + "name": "鑷畾涔夋Ы浣� 9", + "command": "", + "enabled": false + }, + "F22": { + "name": "鑷畾涔夋Ы浣� 10", + "command": "", + "enabled": false + }, + "F23": { + "name": "鑷畾涔夋Ы浣� 11", + "command": "", + "enabled": false + }, + "F24": { + "name": "鑷畾涔夋Ы浣� 12", + "command": "", + "enabled": false + } +} diff --git a/pc_companion/disable_autostart.bat b/pc_companion/disable_autostart.bat new file mode 100644 index 0000000..612d715 --- /dev/null +++ b/pc_companion/disable_autostart.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +python autostart_manager.py disable +pause diff --git a/pc_companion/enable_autostart.bat b/pc_companion/enable_autostart.bat new file mode 100644 index 0000000..c7fd922 --- /dev/null +++ b/pc_companion/enable_autostart.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +python autostart_manager.py enable +pause diff --git a/pc_companion/remotemapper.ahk b/pc_companion/remotemapper.ahk new file mode 100644 index 0000000..a027f77 --- /dev/null +++ b/pc_companion/remotemapper.ahk @@ -0,0 +1,52 @@ +; ============================================================================== +; RemoteMapper AutoHotkey Companion (鏋佽嚧瓒呰交閲� 2MB 鏂规) +; ============================================================================== +#NoEnv +#SingleInstance Force +SetWorkingDir %A_ScriptDir% + +; F13 閿� -> 鎵撳紑璁$畻鍣� +F13:: + Run, calc.exe + return + +; F14 閿� -> 鎵撳紑甯哥敤缃戝潃 +F14:: + Run, https://gemini.google.com + return + +; F15 閿� -> 鎵撳紑璁颁簨鏈� +F15:: + Run, notepad.exe + return + +; F16 閿� -> 杩愯 Python 鑴氭湰 +F16:: + Run, python scripts/demo_task.py + return + +; F17 閿� -> 杩愯鎵瑰鐞嗚剼鏈� +F17:: + Run, scripts/demo_action.bat + return + +; F19 ~ F24 棰勭暀鎵╁睍妲戒綅 +F19:: + ; 鍦ㄦ娣诲姞鎮ㄧ殑鑷畾涔夊懡浠� + return + +F20:: + ; 鍦ㄦ娣诲姞鎮ㄧ殑鑷畾涔夊懡浠� + return + +F21:: + return + +F22:: + return + +F23:: + return + +F24:: + return diff --git a/pc_companion/remotemapper_host.py b/pc_companion/remotemapper_host.py new file mode 100644 index 0000000..bb89630 --- /dev/null +++ b/pc_companion/remotemapper_host.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +""" +RemoteMapper PC Companion Daemon +================================ +Ultra-lightweight Windows background service for RemoteMapper ESP32. +Listens for F13-F24 (and other virtual hotkeys) using native Win32 RegisterHotKey (0.0% CPU usage). +Executes custom local scripts (.bat, .ps1, .py, apps, URLs) asynchronously. +""" + +import sys +import os +import json +import time +import ctypes +import subprocess +import threading +from ctypes import wintypes + +# Ensure Windows environment +if sys.platform != 'win32': + print("[ERROR] RemoteMapper PC Companion only supports Windows.") + sys.exit(1) + +# Win32 Constants +WM_HOTKEY = 0x0312 +MOD_NONE = 0x0000 +MOD_ALT = 0x0001 +MOD_CONTROL = 0x0002 +MOD_SHIFT = 0x0004 +MOD_WIN = 0x0008 +MOD_NOREPEAT = 0x4000 + +# Virtual Key Mapping for F13 - F24 and standard keys +VK_MAP = { + # High F-keys (F13 - F24: standard no-conflict keys) + "F13": 0x7C, "F14": 0x7D, "F15": 0x7E, "F16": 0x7F, + "F17": 0x80, "F18": 0x81, "F19": 0x82, "F20": 0x83, + "F21": 0x84, "F22": 0x85, "F23": 0x86, "F24": 0x87, + # Standard F-keys + "F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73, + "F5": 0x74, "F6": 0x75, "F7": 0x76, "F8": 0x77, + "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B, + # Special keys + "PAUSE": 0x13, "SCROLLLOCK": 0x91, "PRINTSCREEN": 0x2C, + "APPS": 0x5D, "SLEEP": 0x5F +} + +def get_app_dir(): + """Get absolute directory path whether running from Python script or PyInstaller EXE.""" + if getattr(sys, 'frozen', False): + return os.path.dirname(os.path.abspath(sys.executable)) + return os.path.dirname(os.path.abspath(__file__)) + +SCRIPT_DIR = get_app_dir() +CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.json") + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 + +class RemoteMapperDaemon: + def __init__(self, config_path=CONFIG_FILE): + self.config_path = config_path + self.config = {} + self.hotkey_map = {} # id -> (key_name, config_entry) + self.registered_ids = [] + self.last_config_mtime = 0 + self.running = True + + def load_config(self): + """Load JSON configuration mapping hotkeys to local script actions.""" + if not os.path.exists(self.config_path): + self.create_default_config() + + try: + with open(self.config_path, "r", encoding="utf-8") as f: + self.config = json.load(f) + self.last_config_mtime = os.path.getmtime(self.config_path) + print(f"[CONFIG] Loaded {len(self.config)} hotkey action(s) from {os.path.basename(self.config_path)}") + return True + except Exception as e: + print(f"[ERROR] Failed to load config.json: {e}") + return False + + def create_default_config(self): + """Create sample config.json if missing.""" + default_cfg = { + "F13": { + "name": "鎵撳紑璁$畻鍣�", + "command": "calc.exe", + "enabled": True + }, + "F14": { + "name": "鎵撳紑娴忚鍣ㄧ綉鍧�", + "command": "explorer https://gemini.google.com", + "enabled": True + }, + "F15": { + "name": "鎵撳紑璁颁簨鏈�", + "command": "notepad.exe", + "enabled": True + }, + "F16": { + "name": "杩愯鑷畾涔� Python 浠诲姟", + "command": "python scripts/demo_task.py", + "enabled": True + }, + "F17": { + "name": "杩愯鑷畾涔夋壒澶勭悊", + "command": "scripts/demo_action.bat", + "enabled": True + } + } + with open(self.config_path, "w", encoding="utf-8") as f: + json.dump(default_cfg, f, ensure_ascii=False, indent=4) + print(f"[INIT] Created default template: {self.config_path}") + + def register_all_hotkeys(self): + """Register all enabled hotkeys with Windows OS.""" + self.unregister_all_hotkeys() + self.hotkey_map.clear() + hotkey_id = 100 + + for key_str, entry in self.config.items(): + if not entry.get("enabled", True): + continue + + parts = [p.strip().upper() for p in key_str.split("+")] + main_key = parts[-1] + vk = VK_MAP.get(main_key) + + if not vk: + print(f"[WARN] Unknown key: '{main_key}' in '{key_str}' (skipped)") + continue + + mod = MOD_NOREPEAT + for p in parts[:-1]: + if p in ("CTRL", "CONTROL"): mod |= MOD_CONTROL + elif p in ("ALT", "MENU"): mod |= MOD_ALT + elif p in ("SHIFT",): mod |= MOD_SHIFT + elif p in ("WIN", "WINDOWS", "SUPER"): mod |= MOD_WIN + + res = user32.RegisterHotKey(None, hotkey_id, mod, vk) + if res: + self.hotkey_map[hotkey_id] = (key_str, entry) + self.registered_ids.append(hotkey_id) + action_name = entry.get("name", "鏈懡鍚嶅姩浣�") + print(f" [+] 鎴愬姛鐩戝惉: {key_str: <10} -> {action_name}") + hotkey_id += 1 + else: + err = kernel32.GetLastError() + print(f" [-] 娉ㄥ唽澶辫触: {key_str} (鍙兘宸茶鍏朵粬绋嬪簭鍗犵敤, Win32 Error: {err})") + + def unregister_all_hotkeys(self): + """Unregister previously registered hotkeys.""" + for hid in self.registered_ids: + user32.UnregisterHotKey(None, hid) + self.registered_ids.clear() + + def execute_action(self, key_str, entry): + """Execute the configured command asynchronously with robust relative path and Python environment resolution.""" + name = entry.get("name", "鏈懡鍚嶅姩浣�") + cmd = entry.get("command", "").strip() + + if not cmd: + print(f"[TRIGGER] {key_str} 鎸変笅 -> 鏃犻厤缃懡浠�") + return + + # 1. Expand {DIR} or {ROOT} placeholders + cmd = cmd.replace("{DIR}", SCRIPT_DIR).replace("{ROOT}", SCRIPT_DIR) + + # 2. Smart Python path resolution (works even if Python is not in system PATH on end-user PC) + if cmd.startswith("python ") or cmd.startswith("pythonw "): + py_bin = sys.executable + if cmd.startswith("pythonw "): + py_bin = py_bin.replace("python.exe", "pythonw.exe") + cmd = f'"{py_bin}" ' + cmd.split(" ", 1)[1] + + print(f"\n[TRIGGER] >>> 閬ユ帶鍣ㄦ寜涓� {key_str} [{name}] -> 鎵ц鍛戒护: {cmd}") + + def _run(): + try: + # Force working directory to SCRIPT_DIR so all relative paths (scripts/...) work anywhere + subprocess.Popen(cmd, shell=True, cwd=SCRIPT_DIR) + except Exception as ex: + print(f"[ERROR] 鎵ц澶辫触 [{cmd}]: {ex}") + + threading.Thread(target=_run, daemon=True).start() + + def check_config_reload(self): + """Periodically check if config.json was edited by user and hot-reload.""" + try: + if os.path.exists(self.config_path): + mtime = os.path.getmtime(self.config_path) + if mtime > self.last_config_mtime: + print("\n[HOT-RELOAD] 妫�娴嬪埌 config.json 宸蹭慨鏀癸紝姝e湪閲嶆柊杞藉叆閰嶇疆...") + if self.load_config(): + self.register_all_hotkeys() + except Exception: + pass + + def run(self): + """Main message loop with Windows event pump (0% CPU).""" + print("=" * 60) + print(" RemoteMapper PC Companion - 鏋佺畝瓒呰交閲忔湰鍦拌剼鏈墿灞曞畧鎶よ繘绋�") + print(f" 宸ヤ綔鐩綍: {SCRIPT_DIR}") + print("=" * 60) + + self.load_config() + self.register_all_hotkeys() + + print("\n[READY] 姝e湪鍚庡彴鐩戝惉閬ユ帶鍣ㄥ揩鎹烽敭 (0.0% CPU 鏋佷綆寮�閿�)...") + print("[TIP] 闅忔椂缂栬緫 config.json 鍗冲彲瀹炴椂鐑洿鏂帮紝鎸� Ctrl+C 閫�鍑虹▼搴忋�俓n") + + msg = wintypes.MSG() + last_check_time = time.time() + + while self.running: + # Check for config reload every 1.5s + now = time.time() + if now - last_check_time > 1.5: + self.check_config_reload() + last_check_time = now + + # Process any available messages without busy looping + while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): # PM_REMOVE = 1 + if msg.message == WM_HOTKEY: + hid = msg.wParam + if hid in self.hotkey_map: + key_str, entry = self.hotkey_map[hid] + self.execute_action(key_str, entry) + user32.TranslateMessage(ctypes.byref(msg)) + user32.DispatchMessageW(ctypes.byref(msg)) + + # Sleep 30ms to prevent CPU spin + time.sleep(0.03) + + self.unregister_all_hotkeys() + +if __name__ == "__main__": + daemon = RemoteMapperDaemon() + try: + daemon.run() + except KeyboardInterrupt: + print("\n[EXIT] RemoteMapper PC Companion 宸查��鍑恒��") + daemon.unregister_all_hotkeys() diff --git a/pc_companion/scripts/demo_action.bat b/pc_companion/scripts/demo_action.bat new file mode 100644 index 0000000..6babbda --- /dev/null +++ b/pc_companion/scripts/demo_action.bat @@ -0,0 +1,6 @@ +@echo off +echo =================================================== +echo [RemoteMapper] Triggered local batch script! +echo Time: %date% %time% +echo =================================================== +timeout /t 3 diff --git a/pc_companion/scripts/demo_task.py b/pc_companion/scripts/demo_task.py new file mode 100644 index 0000000..4cb00c8 --- /dev/null +++ b/pc_companion/scripts/demo_task.py @@ -0,0 +1,5 @@ +锘�# -*- coding: utf-8 -*- +import datetime + +print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] RemoteMapper Python 鑷畾涔夎剼鏈Е鍙戞垚鍔燂紒") +# 鎮ㄥ彲浠ュ湪杩欓噷缂栧啓浠绘剰鑷姩鍖栭�昏緫锛屼緥濡傝皟鍙栧ぇ妯″瀷 API銆佹搷鎺ц蒋浠躲�佸彂閫佺綉缁滆姹傜瓑 diff --git a/pc_companion/scripts/sample_action.ps1 b/pc_companion/scripts/sample_action.ps1 new file mode 100644 index 0000000..067bde5 --- /dev/null +++ b/pc_companion/scripts/sample_action.ps1 @@ -0,0 +1,3 @@ +锘�# PowerShell 绀轰緥鑴氭湰 +Write-Host "[RemoteMapper] 瑙﹀彂 PowerShell 鑴氭湰" -ForegroundColor Cyan +[System.Media.SystemSounds]::Asterisk.Play() diff --git a/pc_companion/silent_start.vbs b/pc_companion/silent_start.vbs new file mode 100644 index 0000000..17e9b0a --- /dev/null +++ b/pc_companion/silent_start.vbs @@ -0,0 +1,2 @@ +Set WshShell = CreateObject("WScript.Shell") +WshShell.Run "pythonw.exe "" & Replace(WScript.ScriptFullName, "silent_start.vbs", "remotemapper_host.py") & "", 0, False diff --git a/pc_companion/start_companion.bat b/pc_companion/start_companion.bat new file mode 100644 index 0000000..6bdc845 --- /dev/null +++ b/pc_companion/start_companion.bat @@ -0,0 +1,13 @@ +@echo off +cd /d "%~dp0" +cls +echo ======================================================== +echo RemoteMapper PC Companion Service +echo ======================================================== +echo. +python remotemapper_host.py +if errorlevel 1 ( + echo. + echo [ERROR] Python script exited with error. + pause +) diff --git a/src/web/web_ui.h b/src/web/web_ui.h index a90ffdc..51ffa29 100644 --- a/src/web/web_ui.h +++ b/src/web/web_ui.h @@ -434,6 +434,20 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( + + + + + + + + + + + + + + @@ -524,6 +538,8 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( 'Comma': 0x36, 'Period': 0x37, 'Slash': 0x38, 'CapsLock': 0x39, 'F1': 0x3A, 'F2': 0x3B, 'F3': 0x3C, 'F4': 0x3D, 'F5': 0x3E, 'F6': 0x3F, 'F7': 0x40, 'F8': 0x41, 'F9': 0x42, 'F10': 0x43, 'F11': 0x44, 'F12': 0x45, + 'F13': 0x68, 'F14': 0x69, 'F15': 0x6A, 'F16': 0x6B, 'F17': 0x6C, 'F18': 0x6D, + 'F19': 0x6E, 'F20': 0x6F, 'F21': 0x70, 'F22': 0x71, 'F23': 0x72, 'F24': 0x73, 'PrintScreen': 0x46, 'ScrollLock': 0x47, 'Pause': 0x48, 'Insert': 0x49, 'Home': 0x4A, 'PageUp': 0x4B, 'Delete': 0x4C, 'End': 0x4D, 'PageDown': 0x4E, 'ArrowRight': 0x4F, 'ArrowLeft': 0x50, 'ArrowDown': 0x51, 'ArrowUp': 0x52