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>
239 lines
6.8 KiB
JavaScript
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 };
|
|
})();
|