Files
multi-monitor-kiosk/kiosk/server.js
T

227 lines
6.2 KiB
JavaScript

const express = require('express');
const bodyParser = require('body-parser');
const { spawn, execSync, exec } = require('child_process');
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const PORT = process.env.PORT || 5011;
const MONITOR = process.env.MONITOR || 'HDMI-1';
// State variables
let browserProcess = null;
let autorefreshInterval = null;
let currentUrl = process.env.LAUNCH_URL || 'https://duckduckgo.com';
let isKiosk = 1; // Cog is always full-screen/kiosk implicitly
let isGpu = 1; // WPE is inherently hardware-accelerated
function getMonitorGeometry(monitorName) {
try {
const stdout = execSync('xrandr').toString();
const lines = stdout.split('\n');
for (const line of lines) {
if (line.startsWith(monitorName + ' ') && line.includes(' connected')) {
const match = line.match(/(\d+)x(\d+)\+(\d+)\+(\d+)/);
if (match) {
return {
width: match[1],
height: match[2],
x: match[3],
y: match[4]
};
}
}
}
} catch (e) {
console.error('Failed to execute xrandr:', e.message);
}
return null;
}
function ensureMonitorConnected() {
return new Promise(resolve => {
const check = () => {
try {
execSync('xrandr --auto');
} catch(e) {}
const geom = getMonitorGeometry(MONITOR);
if (geom) {
console.log(`Monitor ${MONITOR} is connected and ready.`);
resolve(geom);
} else {
console.log(`Waiting for monitor ${MONITOR} to be connected...`);
setTimeout(check, 5000);
}
};
check();
});
}
function positionCogWindow(geometry) {
// Wait for the window to appear, then use wmctrl to move it
console.log('Attempting to position Cog window...');
let attempts = 10;
const tryMove = () => {
try {
// Find the window ID of the Cog browser.
// Cog's window title or class is usually 'cog'
const stdout = execSync('wmctrl -l').toString();
if (stdout.toLowerCase().includes('cog')) {
// Find WID
const line = stdout.split('\n').find(l => l.toLowerCase().includes('cog'));
if (line) {
const wid = line.split(' ')[0];
console.log(`Found Cog window: ${wid}. Positioning to ${geometry.x},${geometry.y}...`);
// Remove maximization first
try { execSync(`wmctrl -i -r ${wid} -b remove,maximized_vert,maximized_horz`); } catch (e) {}
// Move and resize: gravity,x,y,w,h
execSync(`wmctrl -i -r ${wid} -e 0,${geometry.x},${geometry.y},${geometry.width},${geometry.height}`);
console.log('Window positioned successfully.');
return; // Done
}
}
} catch (e) {
console.log('wmctrl check failed', e.message);
}
attempts--;
if (attempts > 0) {
setTimeout(tryMove, 1000);
} else {
console.error('Failed to find and position Cog window.');
}
};
setTimeout(tryMove, 2000);
}
async function startBrowser() {
console.log(`Checking connection status for ${MONITOR}...`);
const geometry = await ensureMonitorConnected();
if (browserProcess) {
console.log('Stopping existing Cog...');
browserProcess.kill('SIGTERM');
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log(`Starting Cog to navigate to ${currentUrl}`);
// Launch Cog. We use the X11 platform if available.
const args = [currentUrl];
const env = Object.assign({}, process.env, {
COG_PLATFORM_NAME: 'x11',
// Ensures background opacity might work and standard behaviors
});
browserProcess = spawn('cog', args, {
stdio: 'inherit',
env: env
});
// Call position function which will wait for window to exist and move it
positionCogWindow(geometry);
}
// API Endpoints
app.get('/ping', (req, res) => {
res.status(200).send('OK');
});
app.post('/refresh', (req, res) => {
try {
execSync('cogctl reload');
res.status(200).send('Refreshed');
} catch (e) {
res.status(503).send('Browser not ready');
}
});
app.post('/autorefresh/:interval', (req, res) => {
const interval = parseInt(req.params.interval, 10);
if (autorefreshInterval) {
clearInterval(autorefreshInterval);
autorefreshInterval = null;
}
if (interval > 0 && interval <= 60) {
autorefreshInterval = setInterval(() => {
try { execSync('cogctl reload'); } catch(e) {}
}, interval * 1000);
res.status(200).send(`Autorefresh enabled: ${interval}s`);
} else {
res.status(200).send('Autorefresh disabled');
}
});
app.post('/scan', (req, res) => {
res.status(200).send('Scanned');
});
app.get('/url', (req, res) => {
res.status(200).send(currentUrl);
});
app.post('/url', (req, res) => {
if (req.body.url) currentUrl = req.body.url;
// Cogctl can directly tell the browser to open a URL without restarting it
try {
console.log(`Sending cogctl open ${currentUrl}`);
execSync(`cogctl open "${currentUrl}"`);
res.status(200).send(`URL set to ${currentUrl}`);
} catch (e) {
console.error('Failed to set URL via cogctl. Attempting restart...', e.message);
startBrowser().then(() => {
res.status(200).send(`URL set to ${currentUrl} (restarted)`);
});
}
});
app.get('/gpu', (req, res) => {
res.status(200).send('1'); // Always 1 for WPE
});
app.put('/gpu/:value', (req, res) => {
res.status(200).send('1'); // Ignored for WPE
});
app.get('/kiosk', (req, res) => {
res.status(200).send('1'); // Always 1 for WPE
});
app.put('/kiosk/:value', (req, res) => {
res.status(200).send('1'); // Ignored for WPE
});
app.get('/flags', (req, res) => {
res.status(200).json(['WPE WebKit (Cog)']);
});
app.get('/version', (req, res) => {
exec('cog --version', (err, stdout) => {
if (err) res.status(500).send('Unknown');
else res.status(200).send(stdout.trim());
});
});
app.get('/screenshot', (req, res) => {
const file = `/tmp/screenshot-${Date.now()}.png`;
exec(`scrot ${file}`, (err) => {
if (err) return res.status(500).send('Screenshot failed');
res.sendFile(file);
});
});
// Start initially
startBrowser();
app.listen(PORT, () => {
console.log(`Kiosk API running on port ${PORT} for monitor ${MONITOR}`);
});