Compare commits

..

11 Commits
1.0.5 ... main

Author SHA1 Message Date
968a171c6f update build script to use new name 2025-06-09 15:00:27 +01:00
3581873b41 run commands as sudo 2025-06-09 13:51:05 +01:00
5934a79525 add basic api 2025-06-09 13:47:58 +01:00
28d7d22cd5
Update main.yml
only on tag
2025-06-09 13:47:19 +01:00
81385f2f0e
Update main.yml
remove x86 support
2025-06-09 13:46:31 +01:00
cdd2bc2d02 fixes for all services and reverts 2025-06-04 12:59:05 +01:00
8f4762a32d Merge branch 'main' of https://github.com/karl0ss/AnotterKiosk 2025-06-04 10:34:04 +01:00
17913a5da3 not sure why this hasn't been set on the image 2025-06-04 10:34:02 +01:00
dee5a36fda
Update README.md 2025-06-02 16:22:10 +01:00
94cd861c10 remove the locked / root fs 2025-06-02 15:38:08 +01:00
225cb14d3d Fix executable bit on refresh-screen and setup-refresh-timer 2025-06-02 14:18:50 +01:00
12 changed files with 206 additions and 71 deletions

View File

@ -1,43 +1,14 @@
name: CI
on:
create: { }
pull_request: { }
push:
tags:
- '*' # Triggers on any tag push
permissions:
contents: write
jobs:
x86:
runs-on: [ubuntu-latest]
outputs:
pkgfile: ${{ steps.pkgname.outputs.pkgfile }}
steps:
- name: Check out repo
uses: actions/checkout@v2
- name: Install dependencies
run: |
sudo apt update -qq
sudo apt install -yqq libguestfs-tools qemu-utils qemu-system-x86 ovmf qemu-block-extra qemu-user-static binfmt-support rsync sudo wget xz-utils pigz mount dosfstools libarchive-tools
- name: Build firmware
run: |
./build_x86.sh
- name: Release build artifacts
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
append_body: true
body_path: ./version-info
files: |
./anotterkiosk-*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
raspberrypi:
runs-on: [self-hosted]
@ -59,7 +30,6 @@ jobs:
- name: Release build artifacts
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
append_body: true
body_path: ./version-info

View File

@ -1,13 +1,23 @@
AnotterKiosk
=============================
N-AnotterKiosk (Not-AnotterKiosk)
=================================
### I have hacked this about alot from the main branch, mainly Raspberry Pi changes
- Removed x86 support
- Added scheduled screen on/off
- Added scheduled chrome page refresh
- Rpi3 Overclock settings
- Disabled KMS driver for HW screen rotation (screen rotated portrait by default)
### Overview
Another kiosk browser OS? Yes, this one is a little bit opinionated :)
The author ran several similar setups in production for years and has seen a lot of problems and strange failure modes.
This project aims to solve a lot of those (at least for the author), it might also be useful for others :)
#### Key features
- [Images built via CI](https://github.com/Manawyrm/AnotterKiosk/blob/main/.github/workflows/main.yml)
- WiFi connection support
- Raspberry Pi (Arm64) compatibility
@ -22,18 +32,22 @@ This project aims to solve a lot of those (at least for the author), it might al
- SSH support
- VNC support
- SSH tunneling support (for remote-access without port-forwarding, etc.)
- Basic API for Rpi Actions
#### Planned features:
- Raspberry Pi PXE/network boot support
- Network connectivity watchdog (configurable ping, etc. timeout)
- Automatic reboot at specified time
#### Security considerations:
- Autossh does not check SSH host keys. This is okay-ish as long as the target server only allows tunneling, nothing else.
- nginx/PHP are allowed to use sudo/NOPASSWD (because it needs to query the VideoCore, manage service, etc.), more priviledge seperation would be nice
- due to the skeleton mechanism, the system has some ... creative permissions. some cleanup required.
### How-To Use
Like any other Raspberry Pi image: download the current .img file from the [Releases](https://github.com/Manawyrm/AnotterKiosk/releases) page and flash it to a storage device of your choice.
SD cards, USB flash drives, USB SSDs, SATA SSDs, NVMe SSDs are all good options.
You can use a tool like the [Raspberry Pi Imager](https://www.raspberrypi.com/software/), [BalenaEtcher](https://etcher.balena.io/), [Win32DiskImager](https://sourceforge.net/projects/win32diskimager/) or plain "dd" on \*nix-like systems.
@ -46,15 +60,18 @@ Adding your own SSH keys can be done by creating a authorized_keys file.
If you want to use the autossh tunneling features, copy an SSH private key as either "id_rsa" or "id_ed25519".
### HTTP watchdog functionality
Browsers are complex, networks are unstable and software can be buggy.
In order to get the highest reliability possible, self-hosted websites can be modified to include a heartbeat/watchdog functionality.
This works by requesting a certain http-endpoint from the website at some interval.
If your page is being reloaded often (like with a <meta refresh=-header), you can just load the heartbeat-URL as an image:
```html
<img src="http://localhost/heartbeat.php" style="display: none;">
```
If your page stays on one page for a long time (or is just a single-page application), you might want to use AJAX requests to send a heartbeat:
```html
<script>
const req = new XMLHttpRequest();
@ -67,6 +84,46 @@ setInterval(function() {
Whenever the heartbeat stops (for whatever reason), the device will first restart the X11 environment (browser, window manager, etc.) and later (if it hasn't recovered) the whole system by rebooting.
### API
Lightweight HTTP API for controlling and monitoring a Raspberry Pi-based kiosk system. It exposes several endpoints that allow you to query system status, control the display, refresh the screen, and reboot the device — all protected by an API key.
API key will be loaded from `/boot/kioskbrowser.ini`
```ini
[api]
key = "My Key"
```
#### Endpoints
All requests must include a key query parameter matching the API key from the INI file.
`GET /script.php?action=status&key=YOUR_API_KEY`
Returns system status:
```json
{
"temperature": "temp=48.0'C",
"voltage": "volt=1.2000V",
"throttled": "throttled=0x0",
"heartbeat": "2025-06-09 14:33:12"
}
```
`GET /script.php?action=screen_off&key=YOUR_API_KEY`
Turns off the screen.
`GET /script.php?action=screen_on&key=YOUR_API_KEY`
Turns on the screen.
`GET /script.php?action=screen_refresh&key=YOUR_API_KEY`
Starts the screen-refresh.service to refresh the screen.
`GET /script.php?action=reboot&key=YOUR_API_KEY`
Reboots the Raspberry Pi.
### Inspiration / Other Kiosk-OSes:
- https://github.com/jareware/chilipie-kiosk/
- https://github.com/guysoft/FullPageOS

View File

@ -7,10 +7,10 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")"
BUILD_DIR="${SCRIPT_DIR}/work/root/"
# cleanup any previous build attempts
umount -fl "${BUILD_DIR}" || true
losetup -D /dev/loop0 || true
rm -rf "${BUILD_DIR}" || true
mkdir -p "${BUILD_DIR}"
sudo umount -fl "${BUILD_DIR}" || true
sudo losetup -D /dev/loop0 || true
sudo rm -rf "${BUILD_DIR}" || true
sudo mkdir -p "${BUILD_DIR}"
# download a modern RaspiOS build
if [ ! -f raspios.img.xz ]
@ -51,7 +51,7 @@ sed -i 's/vfat defaults/vfat ro,defaults/g' "${BUILD_DIR}/etc/fstab"
sed -i 's/ext4 defaults/ext4 ro,defaults/g' "${BUILD_DIR}/etc/fstab"
# Include git repo version info
echo -n "AnotterKiosk Raspberry Pi version: " > "${BUILD_DIR}/version-info"
echo -n "N-AnotterKiosk Raspberry Pi version: " > "${BUILD_DIR}/version-info"
git describe --abbrev=4 --dirty --always --tags >> "${BUILD_DIR}/version-info"
# Mount system partitions (from the build host)
@ -84,5 +84,5 @@ sudo umount "${BUILD_DIR}"
sudo losetup -D /dev/loop0
tag=$(git describe --abbrev=4 --dirty --always --tags)
mv raspikiosk.img anotterkiosk-${tag}-arm64-raspberrypi.img
pigz -4 anotterkiosk-${tag}-arm64-raspberrypi.img
mv raspikiosk.img n-anotterkiosk-${tag}-arm64-raspberrypi.img
pigz -4 n-anotterkiosk-${tag}-arm64-raspberrypi.img

View File

@ -20,6 +20,10 @@ reboot_time = 04:00
; configure chrome to refresh the page every x minutes
;refresh_screen_every_x_min=15
[api]
; apikey to be sent with commands to /api.php
key = "MyKey"
[wifi]
; If you need more complex WiFi settings (like WPA2-Enterprise, hidden SSIDs, etc.)
; create a file called wpa_supplicant.conf on this partition.

View File

@ -80,7 +80,7 @@ systemctl enable nginx
systemctl enable ssh
systemctl enable kiosk-sechedule-screen.service
systemctl enable schedule-reboot.service
systemctl enable screen-refresh.service
systemctl enable setup-refresh-timer.service
# generate a version info/build info file

View File

@ -1,10 +0,0 @@
[Unit]
Description=Refresh Screen
After=graphical.target
[Service]
Type=oneshot
User=pi
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/pi/.Xauthority
ExecStart=/usr/bin/refresh-screen

View File

@ -0,0 +1,11 @@
[Unit]
Description=Initial screen refresh timer setup
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/setup-refresh-timer
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

0
kiosk_skeleton/usr/bin/refresh-screen Normal file → Executable file
View File

View File

@ -1,6 +1,16 @@
#!/bin/bash
INI_FILE="/boot/kioskbrowser.ini"
remount_root() {
local mode=$1
echo "Remounting root filesystem as $mode..."
mount -o remount,"$mode" / || {
echo "Failed to remount root as $mode"
exit 1
}
}
REBOOT_ENABLED=$(awk -F '=' '/^\[reboot\]/ { in_reboot=1; next }
in_reboot && /^\[/ { in_reboot=0 }
in_reboot && $1 ~ /enabled/ { gsub(/ /, "", $2); print $2 }' "$INI_FILE")
@ -12,6 +22,8 @@ REBOOT_TIME=$(awk -F '=' '/^\[reboot\]/ { in_reboot=1; next }
if [[ "$REBOOT_ENABLED" -eq 1 ]] && [[ "$REBOOT_TIME" =~ ^[0-2][0-9]:[0-5][0-9]$ ]]; then
echo "Scheduling reboot for $REBOOT_TIME..."
remount_root rw
TARGET_TIME=$(date -d "$REBOOT_TIME" +%s)
NOW=$(date +%s)
@ -34,7 +46,6 @@ Persistent=false
WantedBy=timers.target
EOF
# Create the associated service
cat <<EOF > /etc/systemd/system/reboot-at.service
[Unit]
Description=Scheduled Reboot
@ -46,6 +57,8 @@ EOF
systemctl daemon-reload
systemctl enable --now reboot-at.timer
remount_root ro
else
echo "Reboot not scheduled (disabled or invalid time)"
fi

View File

@ -3,6 +3,18 @@
INI_FILE="/boot/kioskbrowser.ini"
SYSTEMD_DIR="/etc/systemd/system"
# Function to safely remount root FS
remount_root() {
local mode=$1
echo "Remounting root filesystem as $mode..."
mount -o remount,"$mode" / || {
echo "Failed to remount root as $mode"
exit 1
}
}
get_ini_value() {
local section=$1 key=$2
awk -F '=' -v sec="$section" -v k="$key" '
@ -17,6 +29,17 @@ get_ini_value() {
create_recurring_timer() {
local action=$1
local time=$2
local value
if [[ "$action" == "on" ]]; then
value=1
elif [[ "$action" == "off" ]]; then
value=0
else
echo "Invalid action: $action"
return 1
fi
local name="screen-${action}"
echo "Setting daily screen ${action} at ${time}"
@ -39,9 +62,7 @@ Description=Turn screen ${action}
[Service]
Type=oneshot
Environment=DISPLAY=:0
ExecStart=/usr/bin/xset dpms force ${action}
Environment=XAUTHORITY=/home/pi/.Xauthority
ExecStart=/usr/bin/vcgencmd display_power ${value}
User=pi
EOF
@ -49,6 +70,7 @@ EOF
systemctl enable --now "${name}.timer"
}
cleanup_screen_timers() {
for action in on off; do
systemctl disable --now screen-${action}.timer 2>/dev/null
@ -57,6 +79,8 @@ cleanup_screen_timers() {
systemctl daemon-reload
}
remount_root rw
# === MAIN ===
SCREEN_ON=$(get_ini_value screen screen_on_time)
SCREEN_OFF=$(get_ini_value screen screen_off_time)
@ -65,3 +89,5 @@ cleanup_screen_timers
[[ "$SCREEN_ON" =~ ^[0-2][0-9]:[0-5][0-9]$ ]] && create_recurring_timer on "$SCREEN_ON"
[[ "$SCREEN_OFF" =~ ^[0-2][0-9]:[0-5][0-9]$ ]] && create_recurring_timer off "$SCREEN_OFF"
remount_root ro

4
kiosk_skeleton/usr/bin/setup-refresh-timer Normal file → Executable file
View File

@ -29,9 +29,13 @@ if [[ "$REFRESH_INTERVAL" =~ ^[0-9]+$ ]] && (( REFRESH_INTERVAL > 0 )); then
cat <<EOF | tee "$SERVICE_UNIT" > /dev/null
[Unit]
Description=Refresh Screen
After=graphical.target
[Service]
Type=oneshot
User=pi
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/pi/.Xauthority
ExecStart=/usr/bin/refresh-screen
EOF

View File

@ -0,0 +1,60 @@
<?php
header('Content-Type: application/json');
// Load API key from INI file
$iniFile = '/boot/kioskbrowser.ini';
if (!file_exists($iniFile)) {
http_response_code(500);
echo json_encode(["error" => "INI file not found"]);
exit;
}
$config = parse_ini_file($iniFile, true);
$API_KEY = trim($config['api']['key'], "\"'"); // Remove any surrounding quotes
// API key check
if (!isset($_GET['key']) || $_GET['key'] !== $API_KEY) {
http_response_code(403);
echo json_encode(["error" => "Forbidden"]);
exit;
}
// Get action
$action = $_GET['action'] ?? '';
switch ($action) {
case 'status':
echo json_encode([
'temperature' => trim(shell_exec("sudo vcgencmd measure_temp")),
'voltage' => trim(shell_exec("sudo vcgencmd measure_volts")),
'throttled' => trim(shell_exec("sudo vcgencmd get_throttled")),
'heartbeat' => date("Y-m-d H:i:s", filemtime("/dev/shm/heartbeat")),
]);
break;
case 'screen_off':
shell_exec("sudo vcgencmd display_power 0");
echo json_encode(["message" => "Screen turned off"]);
break;
case 'screen_on':
shell_exec("sudo vcgencmd display_power 1");
echo json_encode(["message" => "Screen turned on"]);
break;
case 'screen_refresh':
shell_exec("sudo systemctl start screen-refresh.service");
echo json_encode(["message" => "Screen refreshed"]);
break;
case 'reboot':
shell_exec("sudo reboot");
echo json_encode(["message" => "Rebooting"]);
break;
default:
http_response_code(400);
echo json_encode(["error" => "Invalid action"]);
break;
}
?>