are pc bridge · PY
#!/usr/bin/env python3
"""
READYWARE PC Hotkey Bridge
==========================
A tiny local HTTP server that turns READYWARE buttons into PC keystrokes,
mouse actions, typed text, and program launches.
 
How it fits together:
  READYWARE HTTP button  ->  http://<this-pc-ip>:8787/key/f2?token=SECRET
  When fired IN-APP, the phone/tablet on your LAN calls this URL.
  When fired from the WEB VIEWER (anywhere in the world), the command relays
  to your home tablet, and the tablet calls this URL — so it still works, as
  long as your READYWARE device is on the same network as this PC.
 
Setup (once):
  1) Install Python 3 (python.org). During install, tick "Add to PATH".
  2) Install the input library:   pip install pyautogui
  3) Run this file:               python readyware_pc_bridge.py
  4) Note the IP + port it prints, and build READYWARE HTTP buttons to it.
 
Security: LAN-only tool. Keep the TOKEN below secret, don't port-forward this
to the internet, and only run it on a PC you control. /run can launch programs,
so treat the token like a password.
 
URL vocabulary (all GET; add ?token=SECRET if you set one):
  /key/<combo>       press a key or combo   e.g. /key/f2   /key/ctrl+shift+esc   /key/enter
  /type/<text>       type text              e.g. /type/hello%20world    (or /type?text=hello world)
  /hold/<combo>/<ms> hold a key for ms      e.g. /hold/space/1500
  /click/<button>    mouse click            left | right | middle | double   e.g. /click/left
  /move/<x>/<y>      move mouse to x,y       e.g. /move/500/300
  /scroll/<amount>   wheel scroll (+up/-dn)  e.g. /scroll/-5
  /run?path=...      launch a program/.bat   e.g. /run?path=C:\Tools\checkmail.bat
  /ping              health check            returns "READYWARE bridge OK"
"""
 
import sys
import subprocess
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
 
# ─── Config ───────────────────────────────────────────────────────────────
PORT  = 8787
TOKEN = ""          # set a secret string to require ?token=... on every call ("" = no token)
ALLOW_RUN = True    # set False to disable /run (launch programs) entirely
 
# ─── Input backend (pyautogui) ─────────────────────────────────────────────
try:
    import pyautogui
    pyautogui.FAILSAFE = False
except Exception:
    print("!! pyautogui not installed. Run:  pip install pyautogui")
    print("   (keys/mouse/type won't work until you do; /run still works.)")
    pyautogui = None
 
 
def do_key(combo):
    if not pyautogui:
        return "no pyautogui"
    parts = [p.strip().lower() for p in combo.replace(" ", "").split("+") if p.strip()]
    if len(parts) == 1:
        pyautogui.press(parts[0])
    else:
        pyautogui.hotkey(*parts)   # e.g. ctrl+shift+esc
    return "key " + "+".join(parts)
 
 
def do_hold(combo, ms):
    if not pyautogui:
        return "no pyautogui"
    key = combo.strip().lower()
    try:
        secs = max(0, int(ms)) / 1000.0
    except ValueError:
        secs = 0.5
    pyautogui.keyDown(key)
    import time
    time.sleep(secs)
    pyautogui.keyUp(key)
    return f"hold {key} {ms}ms"
 
 
def do_type(text):
    if not pyautogui:
        return "no pyautogui"
    pyautogui.typewrite(text, interval=0.01)
    return "typed %d chars" % len(text)
 
 
def do_click(button):
    if not pyautogui:
        return "no pyautogui"
    b = (button or "left").lower()
    if b == "double":
        pyautogui.doubleClick()
    else:
        pyautogui.click(button=b if b in ("left", "right", "middle") else "left")
    return "click " + b
 
 
def do_move(x, y):
    if not pyautogui:
        return "no pyautogui"
    pyautogui.moveTo(int(x), int(y), duration=0.05)
    return f"move {x},{y}"
 
 
def do_scroll(amount):
    if not pyautogui:
        return "no pyautogui"
    pyautogui.scroll(int(amount))
    return "scroll " + str(amount)
 
 
def do_run(path):
    if not ALLOW_RUN:
        return "run disabled"
    if not path:
        return "no path"
    # shell=True so .bat, associated files, and plain program names all work.
    subprocess.Popen(path, shell=True)
    return "run " + path
 
 
# ─── HTTP handler ──────────────────────────────────────────────────────────
class Handler(BaseHTTPRequestHandler):
    def log_message(self, *a):   # quiet console
        pass
 
    def _send(self, code, msg):
        body = msg.encode("utf-8", "replace")
        self.send_response(code)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Access-Control-Allow-Origin", "*")   # lets a same-LAN browser hit it too
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        try:
            self.wfile.write(body)
        except Exception:
            pass
 
    def do_GET(self):
        try:
            parsed = urllib.parse.urlparse(self.path)
            segs = [urllib.parse.unquote(s) for s in parsed.path.split("/") if s != ""]
            q = urllib.parse.parse_qs(parsed.query)
 
            if TOKEN and (q.get("token", [""])[0] != TOKEN):
                return self._send(403, "bad token")
 
            if not segs or segs[0] == "ping":
                return self._send(200, "READYWARE bridge OK")
 
            cmd = segs[0].lower()
            if cmd == "key" and len(segs) >= 2:
                return self._send(200, do_key(segs[1]))
            if cmd == "hold" and len(segs) >= 3:
                return self._send(200, do_hold(segs[1], segs[2]))
            if cmd == "type":
                text = q.get("text", [None])[0]
                if text is None and len(segs) >= 2:
                    text = "/".join(segs[1:])
                return self._send(200, do_type(text or ""))
            if cmd == "click":
                return self._send(200, do_click(segs[1] if len(segs) >= 2 else "left"))
            if cmd == "move" and len(segs) >= 3:
                return self._send(200, do_move(segs[1], segs[2]))
            if cmd == "scroll" and len(segs) >= 2:
                return self._send(200, do_scroll(segs[1]))
            if cmd == "run":
                path = q.get("path", [None])[0]
                if path is None and len(segs) >= 2:
                    path = "/".join(segs[1:])
                return self._send(200, do_run(path))
 
            return self._send(404, "unknown command: " + cmd)
        except Exception as e:
            return self._send(500, "error: " + str(e))
 
    # POST behaves the same (some HTTP-button setups prefer POST)
    do_POST = do_GET
 
 
def lan_ip():
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
    except Exception:
        ip = "127.0.0.1"
    finally:
        s.close()
    return ip
 
 
if __name__ == "__main__":
    ip = lan_ip()
    print("=" * 58)
    print("  READYWARE PC Hotkey Bridge — running")
    print("  Base URL for your READYWARE buttons:")
    print(f"      http://{ip}:{PORT}")
    if TOKEN:
        print(f"      (append  ?token={TOKEN}  to every button URL)")
    print("  Examples:")
    print(f"      http://{ip}:{PORT}/key/f2")
    print(f"      http://{ip}:{PORT}/key/ctrl+shift+esc")
    print(f"      http://{ip}:{PORT}/type?text=hello")
    print(f"      http://{ip}:{PORT}/run?path=C:\\Tools\\checkmail.bat")
    print("  Stop with Ctrl+C.  Tip: reserve this PC's IP in your router (DHCP).")
    print("=" * 58)
    try:
        ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
    except KeyboardInterrupt:
        print("\nbridge stopped.")
        sys.exit(0)
 