Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/phone_harness/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ def get(key: str, default: str | None = None) -> str | None:
MJPEG_FPS = int(get("MJPEG_FPS", "60") or "60")
MJPEG_QUALITY = int(get("MJPEG_QUALITY", "70") or "70")
MJPEG_SCALE = int(get("MJPEG_SCALE", "50") or "50")

# Viewer poll for /api/phone: seconds between polls. 0 disables the periodic
# poll (viewer will still call loadPhone() on load). Default 0 avoids the
# viewer accidentally triggering a WDA wedge; operators can enable it in
# .env with VIEWER_PHONE_POLL_SECONDS=10 for a 10s poll.
VIEWER_PHONE_POLL_SECONDS = float(get("VIEWER_PHONE_POLL_SECONDS", "0") or "0")
40 changes: 39 additions & 1 deletion src/phone_harness/viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,19 @@ <h2 class="sec">Phone</h2>
setInterval(loadApproval, 1000); // a blocked send must surface fast
loadApproval();
loadGate();
setInterval(loadPhone, 10000);
// Configure periodic polling of /api/phone from server side. Server may
// return 0 to disable the periodic poll (recommended when a device is
// susceptible to WDA heavy-tail wedges). The viewer still calls loadPhone
// once on load so the UI is populated.
fetch('/api/config').then(r => r.json()).then(cfg => {
const s = Number(cfg.phone_poll_seconds) || 0;
if (s > 0) {
setInterval(loadPhone, Math.max(1000, Math.round(s * 1000)));
}
}).catch(() => {
// Fallback: keep the previous 10s poll if config cannot be read.
setInterval(loadPhone, 10000);
});
// Both clamps are measured, so both are wrong the moment the window changes
// size. Re-fit on resize only — never on a poll, or the column would thrash.
let refit;
Expand Down Expand Up @@ -1118,6 +1130,12 @@ <h2 class="sec">Phone</h2>
// ---- keyboard: type on your keyboard, characters go to the phone -----------
let keyBuf = '';
let keyBusy = false;
const ARROW_KEYS = {
ArrowLeft: '\uE012', ArrowRight: '\uE014',
ArrowUp: '\uE013', ArrowDown: '\uE015'
};
let arrowBuf = [];
let arrowBusy = false;
async function flushKeys() {
if (keyBusy || !keyBuf) return;
const text = keyBuf; keyBuf = '';
Expand All @@ -1130,6 +1148,20 @@ <h2 class="sec">Phone</h2>
if (keyBuf) flushKeys(); // send what queued up while we were busy
}
}
async function sendArrowKey(key) {
arrowBuf.push(key);
if (arrowBusy) return;
arrowBusy = true;
try {
while (arrowBuf.length) {
const next = arrowBuf.shift();
await fetch('/api/key', {method:'POST', headers:JSON_HDR,
body: JSON.stringify({key: next})});
}
} finally {
arrowBusy = false;
}
}
window.addEventListener('paste', async (ev) => {
if (ovOpen) return;
if (!inputEnabled || phoneBusy()) return;
Expand Down Expand Up @@ -1168,6 +1200,12 @@ <h2 class="sec">Phone</h2>
}
if (ev.ctrlKey || ev.metaKey || ev.altKey) return; // keep browser shortcuts
if (phoneBusy()) { ev.preventDefault(); return; } // keys aimed at the pre-action screen
const arrow = ARROW_KEYS[ev.key];
if (arrow) {
ev.preventDefault();
sendArrowKey(arrow);
return;
}
let ch = null;
if (ev.key === 'Enter') ch = '\n';
else if (ev.key === 'Backspace') ch = '\b';
Expand Down
15 changes: 15 additions & 0 deletions src/phone_harness/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,17 @@ def do_GET(self): # noqa: vulture
pass
_LAST_PHONE = info
self._json(info)
elif path == "/api/config":
# Viewer configuration (client-visible). phone_poll_seconds is
# the interval in seconds for the viewer to poll /api/phone.
# 0 means disabled; the viewer still calls loadPhone() once on
# load so the UI shows current state.
try:
self._json(
{"phone_poll_seconds": float(config.VIEWER_PHONE_POLL_SECONDS)}
)
except Exception:
self._json({"phone_poll_seconds": 0.0})
elif path == "/api/doctor":
global _LAST_DOCTOR
if _ACTION_LOCK.locked() and _LAST_DOCTOR is not None:
Expand Down Expand Up @@ -654,6 +665,10 @@ def do_POST(self): # noqa: vulture
with _action_slot():
self.client.type_text(str(payload.get("text", "")))
self._json({"ok": True})
elif path == "/api/key":
with _action_slot():
self.client.key_press(str(payload["key"]))
self._json({"ok": True})
elif path == "/api/clipboard":
text = str(payload.get("text", payload.get("content", "")))
try:
Expand Down