INITIAL
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
sway \
|
||||||
|
xwayland \
|
||||||
|
dbus-x11 \
|
||||||
|
libwayland-client0 \
|
||||||
|
libwayland-server0 \
|
||||||
|
libwayland-egl1 \
|
||||||
|
mesa-vulkan-drivers \
|
||||||
|
libvulkan1 \
|
||||||
|
fonts-liberation \
|
||||||
|
x11-xserver-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY sway.conf /etc/sway/config
|
||||||
|
COPY start.sh /start.sh
|
||||||
|
RUN chmod +x /start.sh
|
||||||
|
|
||||||
|
# Need to run as root to access DRM devices on many kiosk setups
|
||||||
|
USER root
|
||||||
|
|
||||||
|
CMD ["/start.sh"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ensure runtime directory exists
|
||||||
|
mkdir -p /run/wayland
|
||||||
|
chmod 0700 /run/wayland
|
||||||
|
|
||||||
|
# Set env vars for Wayland/Sway
|
||||||
|
export XDG_RUNTIME_DIR=/run/wayland
|
||||||
|
export WAYLAND_DISPLAY=wayland-1
|
||||||
|
export WLR_BACKENDS=drm,libinput
|
||||||
|
# Fallback to headless if DRM is not available (useful for testing on mac/desktop without actual DRM)
|
||||||
|
# export WLR_BACKENDS=headless
|
||||||
|
|
||||||
|
# Run sway
|
||||||
|
echo "Starting Sway..."
|
||||||
|
exec sway -d -c /etc/sway/config
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Default sway config for Kiosk
|
||||||
|
|
||||||
|
# Disable default bar
|
||||||
|
bar {
|
||||||
|
mode invisible
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove window borders and title bars
|
||||||
|
default_border none
|
||||||
|
default_floating_border none
|
||||||
|
hide_edge_borders both
|
||||||
|
|
||||||
|
# Map workspaces to specific outputs
|
||||||
|
# Note: In a real Pi 4 environment, the outputs might be named HDMI-A-1 and HDMI-A-2.
|
||||||
|
# You can customize these output names based on `swaymsg -t get_outputs`.
|
||||||
|
workspace 1 output HDMI-A-1
|
||||||
|
workspace 2 output HDMI-A-2
|
||||||
|
|
||||||
|
# Set a black background
|
||||||
|
output * bg #000000 solid_color
|
||||||
|
|
||||||
|
# Idle configuration
|
||||||
|
# We don't want the screen to turn off in a kiosk.
|
||||||
|
exec swayidle -w \
|
||||||
|
timeout 3600 'swaymsg "output * dpms on"' \
|
||||||
|
resume 'swaymsg "output * dpms on"'
|
||||||
|
|
||||||
|
# Include any other specific configurations
|
||||||
|
include /etc/sway/config.d/*
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
display-manager:
|
||||||
|
build: ./display-manager
|
||||||
|
privileged: true
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- wayland-socket:/run/wayland
|
||||||
|
- /dev:/dev
|
||||||
|
environment:
|
||||||
|
- XDG_RUNTIME_DIR=/run/wayland
|
||||||
|
- WAYLAND_DISPLAY=wayland-1
|
||||||
|
|
||||||
|
kiosk-1:
|
||||||
|
build: ./kiosk
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- display-manager
|
||||||
|
ports:
|
||||||
|
- "5011:5011"
|
||||||
|
volumes:
|
||||||
|
- wayland-socket:/run/wayland
|
||||||
|
environment:
|
||||||
|
- XDG_RUNTIME_DIR=/run/wayland
|
||||||
|
- WAYLAND_DISPLAY=wayland-1
|
||||||
|
- SWAYSOCK=/run/wayland/sway-ipc.sock
|
||||||
|
- WORKSPACE=1
|
||||||
|
- PORT=5011
|
||||||
|
- LAUNCH_URL=https://github.com
|
||||||
|
- KIOSK=1
|
||||||
|
- GPU=1
|
||||||
|
|
||||||
|
kiosk-2:
|
||||||
|
build: ./kiosk
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- display-manager
|
||||||
|
ports:
|
||||||
|
- "5012:5012"
|
||||||
|
volumes:
|
||||||
|
- wayland-socket:/run/wayland
|
||||||
|
environment:
|
||||||
|
- XDG_RUNTIME_DIR=/run/wayland
|
||||||
|
- WAYLAND_DISPLAY=wayland-1
|
||||||
|
- SWAYSOCK=/run/wayland/sway-ipc.sock
|
||||||
|
- WORKSPACE=2
|
||||||
|
- PORT=5012
|
||||||
|
- LAUNCH_URL=https://google.com
|
||||||
|
- KIOSK=1
|
||||||
|
- GPU=1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
wayland-socket:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:18-bullseye-slim
|
||||||
|
|
||||||
|
# Install dependencies for Chromium, Wayland, and screenshot tool
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
chromium \
|
||||||
|
sway \
|
||||||
|
grim \
|
||||||
|
jq \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY server.js ./
|
||||||
|
COPY start.sh /start.sh
|
||||||
|
RUN chmod +x /start.sh
|
||||||
|
|
||||||
|
# Provide a default user data directory
|
||||||
|
RUN mkdir -p /data/chromium && chown -R node:node /data
|
||||||
|
|
||||||
|
CMD ["/start.sh"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "kiosk-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Multi-monitor kiosk API based on balena browser block",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"puppeteer-core": "^20.0.0",
|
||||||
|
"body-parser": "^1.20.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const puppeteer = require('puppeteer-core');
|
||||||
|
const { spawn, exec } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 5011;
|
||||||
|
const WORKSPACE = process.env.WORKSPACE || '1';
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
let browserProcess = null;
|
||||||
|
let browser = null;
|
||||||
|
let page = null;
|
||||||
|
let autorefreshInterval = null;
|
||||||
|
|
||||||
|
let currentUrl = process.env.LAUNCH_URL || 'about:blank';
|
||||||
|
let isKiosk = process.env.KIOSK !== '0'; // default 1
|
||||||
|
let isGpu = process.env.GPU !== '0'; // default 1
|
||||||
|
let chromiumFlags = [];
|
||||||
|
|
||||||
|
function buildFlags() {
|
||||||
|
const flags = [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--remote-debugging-port=9222',
|
||||||
|
'--enable-features=UseOzonePlatform',
|
||||||
|
'--ozone-platform=wayland',
|
||||||
|
`--user-data-dir=/data/chromium-${WORKSPACE}`,
|
||||||
|
'--window-position=0,0'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isKiosk) {
|
||||||
|
flags.push('--kiosk');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGpu) {
|
||||||
|
flags.push('--disable-gpu');
|
||||||
|
flags.push('--disable-software-rasterizer');
|
||||||
|
} else {
|
||||||
|
flags.push('--enable-gpu');
|
||||||
|
}
|
||||||
|
|
||||||
|
chromiumFlags = flags;
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startChromium() {
|
||||||
|
if (browserProcess) {
|
||||||
|
console.log('Stopping existing Chromium...');
|
||||||
|
browserProcess.kill('SIGTERM');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
const flags = buildFlags();
|
||||||
|
// Start with a specific title so we can target it with swaymsg
|
||||||
|
const initialUrl = `data:text/html,<html><head><title>KioskInit${WORKSPACE}</title></head><body>Loading...</body></html>`;
|
||||||
|
|
||||||
|
console.log('Starting Chromium with flags:', flags.join(' '));
|
||||||
|
|
||||||
|
browserProcess = spawn('/usr/bin/chromium', [...flags, initialUrl], {
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a bit for the window to appear in Sway
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`Assigning window to workspace ${WORKSPACE}...`);
|
||||||
|
exec(`swaymsg "[title=KioskInit${WORKSPACE}] move to workspace ${WORKSPACE}"`, (err, stdout, stderr) => {
|
||||||
|
if (err) console.error('Swaymsg error:', stderr);
|
||||||
|
else console.log('Swaymsg success:', stdout);
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Connect Puppeteer
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Connecting puppeteer...');
|
||||||
|
browser = await puppeteer.connect({
|
||||||
|
browserURL: 'http://localhost:9222',
|
||||||
|
defaultViewport: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const pages = await browser.pages();
|
||||||
|
page = pages.length > 0 ? pages[0] : await browser.newPage();
|
||||||
|
|
||||||
|
console.log(`Navigating to ${currentUrl}`);
|
||||||
|
await page.goto(currentUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Puppeteer connect error:', e);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Endpoints
|
||||||
|
|
||||||
|
app.get('/ping', (req, res) => {
|
||||||
|
res.status(200).send('OK');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/refresh', async (req, res) => {
|
||||||
|
if (page) {
|
||||||
|
await page.reload();
|
||||||
|
res.status(200).send('Refreshed');
|
||||||
|
} else {
|
||||||
|
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(() => {
|
||||||
|
if (page) page.reload().catch(console.error);
|
||||||
|
}, interval * 1000);
|
||||||
|
res.status(200).send(`Autorefresh enabled: ${interval}s`);
|
||||||
|
} else {
|
||||||
|
res.status(200).send('Autorefresh disabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/scan', (req, res) => {
|
||||||
|
// Stub for local service discovery
|
||||||
|
res.status(200).send('Scanned');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/url', (req, res) => {
|
||||||
|
res.status(200).send(currentUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/url', async (req, res) => {
|
||||||
|
let needRestart = false;
|
||||||
|
|
||||||
|
if (req.body.url) currentUrl = req.body.url;
|
||||||
|
|
||||||
|
if (req.body.gpu !== undefined) {
|
||||||
|
const newGpu = req.body.gpu === '1';
|
||||||
|
if (newGpu !== isGpu) {
|
||||||
|
isGpu = newGpu;
|
||||||
|
needRestart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.kiosk !== undefined) {
|
||||||
|
const newKiosk = req.body.kiosk === '1';
|
||||||
|
if (newKiosk !== isKiosk) {
|
||||||
|
isKiosk = newKiosk;
|
||||||
|
needRestart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needRestart) {
|
||||||
|
await startChromium();
|
||||||
|
} else if (page) {
|
||||||
|
await page.goto(currentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(`URL set to ${currentUrl}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/gpu', (req, res) => {
|
||||||
|
res.status(200).send(isGpu ? '1' : '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/gpu/:value', async (req, res) => {
|
||||||
|
const val = req.params.value === '1';
|
||||||
|
if (val !== isGpu) {
|
||||||
|
isGpu = val;
|
||||||
|
await startChromium();
|
||||||
|
}
|
||||||
|
res.status(200).send(isGpu ? '1' : '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/kiosk', (req, res) => {
|
||||||
|
res.status(200).send(isKiosk ? '1' : '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/kiosk/:value', async (req, res) => {
|
||||||
|
const val = req.params.value === '1';
|
||||||
|
if (val !== isKiosk) {
|
||||||
|
isKiosk = val;
|
||||||
|
await startChromium();
|
||||||
|
}
|
||||||
|
res.status(200).send(isKiosk ? '1' : '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/flags', (req, res) => {
|
||||||
|
res.status(200).json(chromiumFlags);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/version', (req, res) => {
|
||||||
|
exec('/usr/bin/chromium --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`;
|
||||||
|
// Using grim instead of scrot for Wayland
|
||||||
|
// We specify the output by workspace name if needed, but grim captures the whole screen if no output is specified.
|
||||||
|
// To capture just the workspace, we can use grim with swaymsg to find the coordinates,
|
||||||
|
// or simply rely on grim which by default captures everything.
|
||||||
|
// To be safe and capture just the output for this workspace:
|
||||||
|
exec(`grim -o HDMI-A-${WORKSPACE} ${file}`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
// Fallback if HDMI-A-X is not found (e.g. testing locally)
|
||||||
|
exec(`grim ${file}`, (err2) => {
|
||||||
|
if (err2) return res.status(500).send('Screenshot failed');
|
||||||
|
res.sendFile(file);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.sendFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start initially
|
||||||
|
startChromium();
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Kiosk API running on port ${PORT}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Wait for Wayland socket to be available
|
||||||
|
echo "Waiting for Wayland display $WAYLAND_DISPLAY..."
|
||||||
|
while [ ! -e "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Wayland display found. Starting API server..."
|
||||||
|
|
||||||
|
# Start the Node.js API server
|
||||||
|
exec node server.js
|
||||||
Reference in New Issue
Block a user