Files
Mark-VIII/payload/user/remote_access/pager-webui/www/js/pager.js
T
bzuccaroandCursor 7d48b7ad06 fix: harden UI actions and daemon calls for reliable control (v1.3.2)
Retry and serialize pineapd/hak5 calls, queue virtual-pager keys, and grey out buttons until the pager finishes. Deploy now installs python3-light after factory firmware. Bump version to 1.3.2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 15:49:10 -05:00

239 lines
6.8 KiB
JavaScript

'use strict';
const Pager = (() => {
const SCREEN_WIDTH = 480;
const SCREEN_HEIGHT = 222;
const FB_STRIDE = SCREEN_WIDTH * 4;
const PAGER_WIDTH = 745;
const MAX_QUEUED_KEYS = 24;
const KEY_MAP = {
'LEFT.png': 'ArrowLeft',
'UP.png': 'ArrowUp',
'RIGHT.png': 'ArrowRight',
'DOWN.png': 'ArrowDown',
'A_Button.png': 'Enter',
'B_Button.png': 'Escape'
};
let panel = null;
let bodyEl = null;
let table = null;
let pager = null;
let canvas = null;
let screenerr = null;
let keyws = null;
let screenws = null;
let wantOpen = false;
let retryTimer = null;
let retryMs = 400;
const pendingKeys = [];
function ensure() {
if (table) return;
panel = document.getElementById('pager-panel');
bodyEl = document.getElementById('pager-body');
table = document.getElementById('pager_ui');
pager = document.getElementById('pager');
canvas = document.getElementById('pager_canvas');
screenerr = document.getElementById('pager_error');
table.querySelectorAll('img.pager-btn').forEach((img) => {
const src = img.getAttribute('src').split('/').pop();
const key = KEY_MAP[src];
if (!key) return;
img.setAttribute('alt', key.replace('Arrow', ''));
img.addEventListener('pointerdown', (event) => {
event.preventDefault();
press(img, key);
});
});
const retry = document.getElementById('screen_retry');
if (retry) retry.addEventListener('click', () => {
retryMs = 400;
connect();
});
}
function press(el, key) {
el.classList.add('pressed');
setTimeout(() => el.classList.remove('pressed'), 80);
sendKey(key);
}
function queueKey(k) {
pendingKeys.push(k);
while (pendingKeys.length > MAX_QUEUED_KEYS) pendingKeys.shift();
}
function flushKeys() {
while (pendingKeys.length && keyws && keyws.readyState === WebSocket.OPEN) {
try { keyws.send(pendingKeys.shift()); }
catch (e) { break; }
}
}
function sendKey(k) {
if (keyws && keyws.readyState === WebSocket.OPEN) {
try { keyws.send(k); return true; }
catch (e) {}
}
queueKey(k);
if (wantOpen) connectKeys();
return false;
}
function renderRGBAFrame(bytes) {
if (bytes.length < FB_STRIDE * SCREEN_HEIGHT) {
console.warn('pager frame too small for 480x222', bytes.length);
return;
}
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT);
const dst = imageData.data;
let di = 0;
for (let y = 0; y < SCREEN_HEIGHT; y++) {
const rowStart = y * FB_STRIDE;
for (let x = 0; x < SCREEN_WIDTH; x++) {
const si = rowStart + x * 4;
dst[di] = bytes[si];
dst[di + 1] = bytes[si + 1];
dst[di + 2] = bytes[si + 2];
dst[di + 3] = bytes[si + 3];
di += 4;
}
}
ctx.putImageData(imageData, 0, 0);
pager.src = canvas.toDataURL('image/png');
}
function showError(show) {
if (screenerr) screenerr.hidden = !show;
}
function scheduleReconnect() {
if (!wantOpen) return;
clearTimeout(retryTimer);
retryTimer = setTimeout(() => {
if (!wantOpen) return;
connectScreen();
connectKeys();
}, retryMs);
retryMs = Math.min(5000, Math.max(400, retryMs * 2));
}
function connectScreen() {
if (!wantOpen) return;
if (screenws && (screenws.readyState === WebSocket.OPEN || screenws.readyState === WebSocket.CONNECTING)) return;
try {
const sock = new WebSocket(App.pagerScreenWs);
sock.binaryType = 'arraybuffer';
screenws = sock;
sock.onopen = () => {
retryMs = 400;
if (keyws && keyws.readyState === WebSocket.OPEN) showError(false);
};
sock.onmessage = (ev) => {
showError(false);
if (ev.data instanceof ArrayBuffer) renderRGBAFrame(new Uint8Array(ev.data));
else if (ev.data && ev.data.arrayBuffer) ev.data.arrayBuffer().then((b) => renderRGBAFrame(new Uint8Array(b)));
};
sock.onerror = () => { showError(true); };
sock.onclose = () => {
if (screenws === sock) screenws = null;
showError(true);
scheduleReconnect();
};
} catch (e) {
showError(true);
scheduleReconnect();
}
}
function connectKeys() {
if (!wantOpen) return;
if (keyws && (keyws.readyState === WebSocket.OPEN || keyws.readyState === WebSocket.CONNECTING)) return;
try {
const sock = new WebSocket(App.pagerKeysWs);
keyws = sock;
sock.onopen = () => {
retryMs = 400;
flushKeys();
if (screenws && screenws.readyState === WebSocket.OPEN) showError(false);
};
sock.onclose = () => {
if (keyws === sock) keyws = null;
scheduleReconnect();
};
sock.onerror = () => { try { sock.close(); } catch (e2) {} };
} catch (e) {
keyws = null;
scheduleReconnect();
}
}
function connect() {
connectScreen();
connectKeys();
}
function disconnect() {
wantOpen = false;
clearTimeout(retryTimer);
retryTimer = null;
pendingKeys.length = 0;
if (screenws) { try { screenws.close(); } catch (e) {} screenws = null; }
if (keyws) { try { keyws.close(); } catch (e) {} keyws = null; }
}
function applyScale() {
if (!bodyEl || !table) return;
const availableWidth = bodyEl.clientWidth - 20;
const scale = Math.min(1, availableWidth / PAGER_WIDTH);
table.style.zoom = String(scale);
table.style.marginBottom = '';
}
function toggle() {
ensure();
if (panel.classList.contains('hidden')) {
panel.classList.remove('hidden');
document.getElementById('pager-btn').classList.add('active');
applyScale();
wantOpen = true;
retryMs = 400;
connect();
try { pager.focus(); } catch (e) {}
} else {
panel.classList.add('hidden');
document.getElementById('pager-btn').classList.remove('active');
disconnect();
}
}
function isOpen() {
return !!panel && !panel.classList.contains('hidden');
}
function close() {
if (isOpen()) toggle();
}
window.addEventListener('resize', () => {
if (table && panel && !panel.classList.contains('hidden')) applyScale();
});
document.addEventListener('keydown', (e) => {
if (!isOpen()) return;
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT'
|| t.tagName === 'BUTTON' || t.tagName === 'A' || t.isContentEditable)) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
const keys = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'Enter', 'Escape'];
if (keys.indexOf(e.key) === -1) return;
if (e.key.indexOf('Arrow') === 0) e.preventDefault();
if (!e.repeat) sendKey(e.key);
});
return { toggle, close, isOpen };
})();