Compare commits

...

3 Commits

Author SHA1 Message Date
nearxos
0844adbcbe Update cloud-init scripts and documentation for enhanced DNS management and provisioning steps</message>
<message>Modify the first-boot.sh script to include an additional step for managing screen brightness during the provisioning process. Update user-data.bootstrap to improve DNS configuration by ensuring NetworkManager manages /etc/resolv.conf correctly, and remove obsolete scripts related to systemd-resolved. Enhance documentation to reflect these changes and clarify the setup process for users, improving overall network boot functionality and user experience.
2026-03-06 14:45:23 +02:00
nearxos
8233304ee2 Update documentation and .gitignore for improved deployment clarity and log management</message>
<message>Enhance the README files to provide clearer instructions for deploying the CM4 eMMC provisioning service on Proxmox, including detailed prerequisites and deployment steps. Update the .gitignore to exclude deploy logs generated during the deployment process, ensuring a cleaner repository. Additionally, refine archived documentation for better historical context and clarity on the active provisioning workflow.
2026-03-04 19:43:52 +02:00
nearxos
2a6355033e Remove obsolete files related to provisioning and custom scripts</message>
<message>Delete the start.elf file and several log files from the emmc-provisioning scripts, which are no longer needed for the deployment process. Additionally, remove the plymouth-custom.script file from the cloud-init duplicates archive, streamlining the project and reducing clutter in the repository.
2026-03-04 19:43:21 +02:00
32 changed files with 2430 additions and 284 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
*.img *.img
!emmc-provisioning/network-boot-initramfs/*.img !emmc-provisioning/network-boot-initramfs/*.img
# Deploy logs (generated by deploy-to-proxmox.sh when DEPLOY_LOG=1)
emmc-provisioning/scripts/deploy-*.log
# Backup/data from devices (large DBs and logs) # Backup/data from devices (large DBs and logs)
backup-from-device/**/data/*.db backup-from-device/**/data/*.db
backup-from-device/**/logs/ backup-from-device/**/logs/

View File

@@ -22,6 +22,7 @@ A single **revision number** is kept in `REVISION` and in a comment line in trac
## Quick start ## Quick start
1. Read **emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md** for full setup. 1. **New deployment:** Follow **[emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md](emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md)** for step-by-step instructions (Proxmox host prep → LXC deploy → network boot → portal files).
2. Use **emmc-provisioning/scripts/sync-portal-files-to-lxc.sh** to sync first-boot assets (including kiosk) to the file server. 2. **Sync first-boot assets** to the file server after deploy:
3. Provision devices via USB boot or network boot; first-boot configures kiosk, labwc, rotation, wallpaper, dark theme, and optional CM4 boot order. `./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>`
3. Provision devices via USB boot or network boot; first-boot configures the Chromium kiosk, labwc Wayland desktop, screen rotation, wallpaper, dark theme, and CM4 boot order.

View File

@@ -1,10 +1,9 @@
# Archive # Archive
This folder holds files that are no longer part of the active reTerminal DM4 / eMMC provisioning workflow. Kept for reference only. This folder holds files that are no longer part of the active reTerminal DM4 / eMMC provisioning workflow. Kept for historical reference only.
| Subfolder | Contents | | Subfolder | Contents |
|-----------|----------| |-----------|----------|
| **chromium-setup-legacy/** | Old Chromium-setup guides and scripts: KDE installation, LED/buzzer control, audio config, touchscreen options, Flask apps, test scripts, revert-to-lxde. Kiosk assets (start-chromium.sh, chromium-kiosk.desktop) live in `emmc-provisioning/cloud-init/` and `emmc-provisioning/cloud-init/config-files/`. | | **chromium-setup-legacy/** | Old Chromium-setup guides and scripts: KDE installation, LED/buzzer control, audio config, touchscreen options, Flask apps, test scripts, revert-to-lxde. The active kiosk launcher lives at `emmc-provisioning/cloud-init/fileserver/start-chromium.sh` (Wayland/labwc). |
| **cloud-init-duplicates/** | Duplicate or superseded cloud-init files (e.g. plymouth-custom.script duplicate of `files-from-guard/plymouth-custom/custom.script`). |
Do not rely on archived files for deployment; use the main tree under **emmc-provisioning/**. Do not rely on archived files for deployment; use the active tree under **emmc-provisioning/**.

View File

@@ -1,40 +0,0 @@
screen_width = Window.GetWidth();
screen_height = Window.GetHeight();
theme_image = Image("splash.png");
image_width = theme_image.GetWidth();
image_height = theme_image.GetHeight();
scale_x = image_width / screen_width;
scale_y = image_height / screen_height;
if (scale_x > 1 || scale_y > 1)
{
if (scale_x > scale_y)
{
resized_image = theme_image.Scale(screen_width, image_height / scale_x);
image_x = 0;
image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2;
}
else
{
resized_image = theme_image.Scale(image_width / scale_y, screen_height);
image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2;
image_y = 0;
}
}
else
{
resized_image = theme_image.Scale(image_width, image_height);
image_x = (screen_width - image_width) / 2;
image_y = (screen_height - image_height) / 2;
}
if (Plymouth.GetMode() != "shutdown")
{
sprite = Sprite(resized_image);
sprite.SetPosition(image_x, image_y, -100);
}
fun message_callback(text) {
}

View File

@@ -13,12 +13,19 @@ Revisions are tracked project-wide; see repo root **README.md** and `scripts/bum
emmc-provisioning/ emmc-provisioning/
├── README.md ← You are here ├── README.md ← You are here
├── docs/ Documentation ├── docs/ Documentation
│ ├── DEPLOY-NEW-PROXMOX.md Step-by-step: deploy to a new Proxmox instance │ ├── DEPLOY-NEW-PROXMOX.md ★ START HERE: full deploy guide (host prep, LXC, scripts, network boot)
│ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage │ ├── EMMC-PROVISIONING-GUIDE.md Golden image creation, cloud-init, PiShrink
│ ├── NETWORK-BOOT-LXC.md Network boot (PXE/dnsmasq) and LXC │ ├── PROXMOX-LXC-DEPLOYMENT.md Reference: what is deployed, redeploy, troubleshooting
│ ├── NETWORK-BOOT-LXC.md Network boot architecture (PXE/dnsmasq, interfaces)
│ ├── NETWORK-BOOT-DEPLOYMENT-FLOW.md Full data flow for network boot provisioning
│ ├── NETWORK-BOOT-TROUBLESHOOTING.md Troubleshooting network boot issues
│ ├── DEVICE-DNS-DHCP-RESOLVCONF.md Device DNS from DHCP, resolv.conf, cloud-init │ ├── DEVICE-DNS-DHCP-RESOLVCONF.md Device DNS from DHCP, resolv.conf, cloud-init
│ ├── DNSMASQ-DNS-FILESERVER.md dnsmasq DNS and file.server on LXC │ ├── DNSMASQ-DNS-FILESERVER.md dnsmasq DNS and file.server on LXC
│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup (reference) │ ├── PROXMOX-HOST-COMPARISON.md Diff between Proxmox hosts (fixes checklist)
│ ├── PREPARE-IMAGE-FOR-CLOUDINIT.md How to shrink and prep a golden image
│ ├── BACKUP-DEVICE-CONFIG-AUDIT.md Audit of backup image contents
│ ├── DEVICE-REMOVABLE-PACKAGES.md Packages to purge from the device image
│ ├── EDIT-CLOUDINIT-ON-DEVICE.md Edit NoCloud files on-device or in .img.xz
│ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference │ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference
├── host/ Scripts that run on the provisioning host (Proxmox host) ├── host/ Scripts that run on the provisioning host (Proxmox host)
│ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd │ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd
@@ -55,8 +62,8 @@ emmc-provisioning/
## Quick start ## Quick start
1. **Read** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for setup and usage. 1. **New deployment:** Follow **[docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md)** — covers Proxmox host prep, LXC creation, host scripts, network boot, and portal file sync.
2. **Deploy to a new Proxmox:** Follow [docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md) for clear step-by-step instructions. 2. **Full setup reference:** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for golden image creation, cloud-init, PiShrink.
3. **Proxmox reference:** [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) and [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md) for options, layout, and troubleshooting. 3. **Redeploy / update:** Re-run `scripts/deploy-to-proxmox.sh root@HOST` — updates scripts, dashboard, udev, and systemd without touching your golden image or enabled flag.
4. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide). 4. **Sync portal files:** After deploy or when kiosk/first-boot assets change: `scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>`.
5. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**. 5. **Troubleshooting:** [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md) for USB errors, rpiboot failures, and monitoring. [docs/NETWORK-BOOT-TROUBLESHOOTING.md](docs/NETWORK-BOOT-TROUBLESHOOTING.md) for network boot issues.

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Local HTTP API for screen-brightness override (crew manual control per MSC.191(79)).
Listens on 127.0.0.1 only. Run as root so it can write /run/screen-brightness/override.
"""
import os
import urllib.parse
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
OVERRIDE_FILE = Path("/run/screen-brightness/override")
RUN_DIR = OVERRIDE_FILE.parent
HOST = "127.0.0.1"
PORT = 8765
class BrightnessHandler(BaseHTTPRequestHandler):
def do_POST(self):
path = self.path.split("?")[0]
if path != "/brightness" and path != "/":
self.send_error(404)
return
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8", errors="ignore") if length else ""
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
level = (query.get("level") or [None])[0] or (urllib.parse.parse_qs(body).get("level") or [None])[0]
if not level:
self.send_response(400)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"missing level=1|2|3|4|5|auto")
return
level = level.strip().lower()
if level == "auto":
value = "auto"
elif level in ("1", "2", "3", "4", "5"):
value = level
else:
self.send_response(400)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"level must be 1, 2, 3, 4, 5, or auto")
return
try:
RUN_DIR.mkdir(parents=True, exist_ok=True)
OVERRIDE_FILE.write_text(value + "\n")
except Exception as e:
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"write failed: {e}".encode())
return
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"ok {value}\n".encode())
def do_GET(self):
if self.path.split("?")[0] in ("/", "/brightness", "/state"):
state_file = Path("/run/screen-brightness/state")
if state_file.exists():
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(state_file.read_bytes())
else:
self.send_response(404)
self.end_headers()
else:
self.send_error(404)
def log_message(self, format, *args):
pass # quiet; use journal if needed
def main():
if os.geteuid() != 0:
raise SystemExit("brightness-api must run as root to write /run/screen-brightness/override")
server = HTTPServer((HOST, PORT), BrightnessHandler)
server.serve_forever()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,18 @@
[Unit]
Description=Local API for screen brightness override (crew manual control)
Documentation=file:///usr/local/share/doc/screen-brightness/SCREEN-BRIGHTNESS-MANUAL-SETUP.md
After=network.target
Before=lightdm.service
[Service]
Type=simple
ExecStart=/usr/local/bin/brightness-api.py
Restart=on-failure
RestartSec=3
User=root
# Listen on 127.0.0.1 only
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Start the brightness overlay after a short delay so the Wayland/X session
# and compositor are ready (autostart often runs before the display is usable).
SCRIPT_DIR="$(dirname "$0")"
sleep 6
exec "$SCRIPT_DIR/brightness-overlay.py"

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Brightness overlay
Comment=Overlay button to control screen brightness (1-5, Auto)
Exec=/home/pi/brightness-overlay-launch.sh
Hidden=false
NoDisplay=true
X-GNOME-Autostart-enabled=true

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
Brightness control overlay — always-on-top icon in bottom-right; click to show controls.
Uses Wayland layer-shell OVERLAY so it stays above Chromium fullscreen/kiosk.
Requires: brightness-api.service running (listens 127.0.0.1:8765).
"""
import logging
import sys
import urllib.request
import urllib.error
LOG_TAG = "brightness-overlay"
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format=f"{LOG_TAG}: %(message)s",
)
log = logging.getLogger(LOG_TAG)
try:
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib
except Exception as e:
log.error("need PyGObject Gtk: %s", e)
sys.exit(1)
HAS_LAYER_SHELL = False
try:
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import GtkLayerShell
HAS_LAYER_SHELL = True
except Exception:
log.warning("GtkLayerShell not available, using regular window")
API_URL = "http://127.0.0.1:8765/brightness"
API_STATE_URL = "http://127.0.0.1:8765/state"
# Sizes
ICON_SIZE = 44
EXPANDED_WIDTH = 300
PANEL_HEIGHT = 48
def get_current_state():
"""Return (level 15, is_auto). On error return (None, True) so Auto appears active."""
try:
with urllib.request.urlopen(API_STATE_URL, timeout=1) as r:
text = r.read().decode()
level = None
override = "0"
for line in text.strip().splitlines():
line = line.strip()
if line.startswith("level="):
level = line.split("=", 1)[1].strip()
elif line.startswith("override="):
override = line.split("=", 1)[1].strip()
if level and level.isdigit() and 1 <= int(level) <= 5:
return int(level), override == "0"
return (None, True)
except Exception:
return (None, True)
def set_brightness(level):
try:
req = urllib.request.Request(
API_URL,
data=f"level={level}".encode(),
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
with urllib.request.urlopen(req, timeout=2) as r:
log.info("brightness %s: %s", level, r.read().decode().strip())
except urllib.error.URLError as e:
log.warning("API call failed (is brightness-api.service running?): %s", e)
except Exception as e:
log.warning("set_brightness failed: %s", e)
def update_active_buttons(level_buttons, auto_btn, current_level, is_auto):
"""Set .active style on the button that matches current state."""
for i, btn in enumerate(level_buttons):
ctx = btn.get_style_context()
if not is_auto and current_level == i + 1:
ctx.add_class("active")
else:
ctx.remove_class("active")
if auto_btn:
ctx = auto_btn.get_style_context()
if is_auto:
ctx.add_class("active")
else:
ctx.remove_class("active")
def make_window_layer_shell(win):
GtkLayerShell.init_for_window(win)
GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY)
# Anchor only to bottom and right so the surface sits in the bottom-right corner
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.LEFT, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.BOTTOM, True)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.BOTTOM, 12)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.RIGHT, 12)
if hasattr(GtkLayerShell, "set_namespace"):
GtkLayerShell.set_namespace(win, "brightness-overlay")
if hasattr(GtkLayerShell, "set_exclusive_zone"):
GtkLayerShell.set_exclusive_zone(win, 0)
log.info("layer-shell OVERLAY anchored bottom-right (always on top)")
def position_fallback_bottom_right(win, width=None, height=None):
"""Position window in bottom-right (X11: only works with toplevel, not POPUP)."""
if width is None:
width = ICON_SIZE
if height is None:
height = ICON_SIZE
display = Gdk.Display.get_default()
if not display:
return
n = display.get_n_monitors()
monitor = display.get_monitor(0) if n else None
if not monitor:
return
geom = monitor.get_geometry()
x = geom.x + geom.width - width - 12
y = geom.y + geom.height - height - 12
win.move(x, y)
def make_window_fallback(win):
# Toplevel (not POPUP) so we can position and have no decorations on X11
win.set_keep_above(True)
win.set_decorated(False)
win.set_type_hint(Gdk.WindowTypeHint.UTILITY) # tool-style window, often undecorated
# Position after map so WM doesn't override
win.connect("map-event", lambda w, e: position_fallback_bottom_right(w))
log.info("X11 fallback: keep-above, undecorated, bottom-right")
def main():
# Normal toplevel: on X11 we can position it and set_decorated(False) removes title bar.
# (POPUP without parent cannot be positioned on X11 and gets "temporary window" / centering.)
win = Gtk.Window()
win.set_decorated(False)
win.set_resizable(False)
win.set_default_size(ICON_SIZE, ICON_SIZE)
win.set_skip_taskbar_hint(True)
win.set_skip_pager_hint(True)
# Transparent window: need RGBA visual and app-paintable
screen = Gdk.Screen.get_default()
if screen and screen.get_rgba_visual():
win.set_visual(screen.get_rgba_visual())
win.set_app_paintable(True)
# CSS: transparent window and overlay; only subtle hover so icon stays visible
style = """
GtkWindow, window {
background-color: transparent;
background-image: none;
}
.brightness-icon-btn {
background-color: transparent;
border-radius: 8px;
padding: 4px;
min-width: 36px;
min-height: 36px;
}
.brightness-icon-btn:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.brightness-panel {
background-color: transparent;
border-radius: 8px;
padding: 6px;
}
.brightness-panel.expanded {
background-color: rgba(0, 0, 0, 0.5);
}
.brightness-panel button {
min-width: 36px;
font-weight: bold;
}
.brightness-panel button.level-btn.active {
background-color: rgba(255, 255, 255, 0.35);
border: 1px solid rgba(255, 255, 255, 0.6);
}
"""
css = Gtk.CssProvider()
css.load_from_data(style.encode())
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
# Main horizontal box: [revealer with controls] [icon button]
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
# Left part: revealer with Brightness + 15 + Auto
strip_box = Gtk.Box(spacing=6)
strip_box.set_margin_start(8)
strip_box.set_margin_top(6)
strip_box.set_margin_bottom(6)
strip_box.set_margin_end(4)
strip_box.get_style_context().add_class("brightness-panel")
strip_box.pack_start(Gtk.Label(label="Brightness "), False, False, 0)
level_buttons = []
for i in range(1, 6):
btn = Gtk.Button(label=str(i))
btn.get_style_context().add_class("level-btn")
btn.set_can_focus(False)
strip_box.pack_start(btn, False, False, 0)
level_buttons.append(btn)
auto_btn = Gtk.Button(label="Auto")
auto_btn.get_style_context().add_class("level-btn")
auto_btn.set_can_focus(False)
strip_box.pack_start(auto_btn, False, False, 0)
def refresh_active_state():
level, is_auto = get_current_state()
update_active_buttons(level_buttons, auto_btn, level or 3, is_auto)
def on_level_click(btn, level):
set_brightness(level)
# Optimistic update so active state shows immediately
if level == "auto":
update_active_buttons(level_buttons, auto_btn, 3, True)
else:
update_active_buttons(level_buttons, auto_btn, int(level), False)
# Sync from API after a short delay (daemon updates state file periodically)
def delayed_refresh():
refresh_active_state()
return False
GLib.timeout_add(500, delayed_refresh)
for i, btn in enumerate(level_buttons):
btn.connect("clicked", on_level_click, str(i + 1))
auto_btn.connect("clicked", on_level_click, "auto")
revealer = Gtk.Revealer()
revealer.set_reveal_child(False)
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
revealer.set_transition_duration(150)
revealer.add(strip_box)
main_box.pack_start(revealer, False, False, 0)
# Right part: always-visible icon button (click toggles panel)
try:
icon_img = Gtk.Image.new_from_icon_name("display-brightness-symbolic", Gtk.IconSize.BUTTON)
except Exception:
icon_img = Gtk.Label(label="")
icon_btn = Gtk.Button()
icon_btn.get_style_context().add_class("brightness-icon-btn")
icon_btn.set_can_focus(False)
icon_btn.add(icon_img)
icon_btn.set_relief(Gtk.ReliefStyle.NONE)
expanded = [False] # use list so closure can mutate
use_fallback = not (HAS_LAYER_SHELL and GtkLayerShell.is_supported())
def on_icon_clicked(btn):
expanded[0] = not expanded[0]
revealer.set_reveal_child(expanded[0])
ctx = strip_box.get_style_context()
if expanded[0]:
ctx.add_class("expanded")
GLib.idle_add(refresh_active_state)
win.resize(EXPANDED_WIDTH, PANEL_HEIGHT)
if use_fallback:
position_fallback_bottom_right(win, EXPANDED_WIDTH, PANEL_HEIGHT)
else:
ctx.remove_class("expanded")
win.resize(ICON_SIZE, ICON_SIZE)
if use_fallback:
position_fallback_bottom_right(win, ICON_SIZE, ICON_SIZE)
icon_btn.connect("clicked", on_icon_clicked)
main_box.pack_end(icon_btn, False, False, 0)
win.add(main_box)
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
make_window_layer_shell(win)
else:
make_window_fallback(win)
win.connect("destroy", Gtk.main_quit)
win.show_all()
GLib.idle_add(refresh_active_state)
log.info("brightness icon visible (click to show controls)")
Gtk.main()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
# screen-brightness.conf — reTerminal DM Bridge Brightness Daemon
#
# Regulatory basis
# IMO MSC.191(79) §8.1 : brightness adjustable, never extinguished
# IMO MSC.302(87) : alarms = high luminance; night mode preserves night vision
# IHO S-52 / IEC 62288 : NIGHT / DUSK / DAY operating modes
#
# Reload without restart: systemctl reload screen-brightness
# Override from bridge console:
# echo 3 | sudo tee /run/screen-brightness/override # fix at level 3
# echo auto | sudo tee /run/screen-brightness/override # return to auto
[screen-brightness]
# ── Sysfs paths ──────────────────────────────────────────────────────────────
backlight_path = /sys/class/backlight/lcd_backlight/brightness
buzzer_path = /sys/class/leds/usr-buzzer/brightness
lux_path = /sys/bus/iio/devices/iio:device0/in_illuminance_input
# ── Timing ───────────────────────────────────────────────────────────────────
# How often the ambient sensor is read (seconds)
poll_interval = 2
# How often the buzzer is checked (seconds). Keep below 1 so alarms are seen quickly.
buzzer_poll = 0.5
# How long to hold maximum brightness after the buzzer stops (seconds)
# IMO MSC.302(87): alarm attention must persist long enough for crew response
buzzer_cooldown = 10
# ── Lux thresholds — IHO S-52 three-mode framework ──────────────────────────
# Adjust these to match the actual lighting environment of your bridge.
#
# 0 — lux_night_max → NIGHT mode (very dark, night watch)
# lux_night_max+1 — lux_dusk_max → DUSK mode (dim, dawn/dusk)
# lux_dusk_max+1 — lux_day_dim_max → DAY-DIM
# lux_day_dim_max+1 — lux_day_normal_max → DAY-NORMAL
# > lux_day_normal_max → DAY-BRIGHT (direct sunlight / tropical day)
lux_night_max = 20
lux_dusk_max = 200
lux_day_dim_max = 500
lux_day_normal_max = 1000
# ── Brightness levels per mode (1 = minimum, 5 = maximum) ───────────────────
# Level 0 is NEVER used — IMO "not to extinction" principle.
# Minimum must remain legible at the navigator's position.
level_night = 1
level_dusk = 2
level_day_dim = 3
level_day_normal = 4
level_day_bright = 5
# Brightness forced during ALERT (buzzer ON) and COOLDOWN states.
# IMO MSC.302(87): alarm visual indicators must be high luminous intensity.
level_alert = 5
# ── Hysteresis ───────────────────────────────────────────────────────────────
# Number of consecutive polls that must agree on a new ambient brightness
# target before the change is committed. Prevents flickering when lux hovers
# near a mode boundary (e.g. sunset making the sensor oscillate 1822 lux).
# At poll_interval=2s and hysteresis_polls=3, a level change takes max 6s.
hysteresis_polls = 3

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
reTerminal DM — Bridge Screen Brightness Daemon
================================================
Regulatory alignment
IMO MSC.191(79) §8.1 Display must be dimmable but NEVER extinguished
§8.1.2 Navigator must be able to reset to a preset level
IMO MSC.302(87) Alarm visuals = high luminance; night mode must not
degrade crew night vision
IHO S-52 / IEC 62288 Three operating modes: NIGHT · DUSK · DAY
Operating states
NIGHT Very low lux → level 1 (minimum — "not to extinction")
DUSK Low lux → level 2 (dim)
DAY-* Daylight → levels 3-5 graduated by lux sub-range
ALERT Buzzer ON → level 5 (max) immediately — no smooth stepping
COOLDOWN Buzzer just → hold level 5 for BUZZER_COOLDOWN_SEC seconds,
stopped then fade back one step per poll
OVERRIDE Manual file → fixed level set by navigator (per MSC.191(79))
Sysfs paths (reTerminal DM)
Backlight : /sys/class/backlight/lcd_backlight/brightness (0-5)
Buzzer : /sys/class/leds/usr-buzzer/brightness (0..max, non-zero = ON; often 255)
Light IIO : /sys/bus/iio/devices/iio:device0/in_illuminance_input (lux)
Navigator override (satisfies MSC.191(79) §8.1.2 preset requirement)
echo 3 > /run/screen-brightness/override # force fixed level 3
echo auto > /run/screen-brightness/override # return to ambient control
Read current state (useful for dashboard integration)
cat /run/screen-brightness/state
Config file : /etc/screen-brightness.conf (INI, optional)
Reload config: kill -HUP <pid> (or: systemctl reload screen-brightness)
"""
import configparser
import logging
import logging.handlers
import os
import signal
import sys
import time
from pathlib import Path
from typing import Optional, Tuple
# ── Default configuration ────────────────────────────────────────────────────
DEFAULTS: dict = {
# sysfs paths
"backlight_path": "/sys/class/backlight/lcd_backlight/brightness",
"buzzer_path": "/sys/class/leds/usr-buzzer/brightness",
"lux_path": "/sys/bus/iio/devices/iio:device0/in_illuminance_input",
# timing
"poll_interval": "2", # seconds between lux/ambient reads
"buzzer_poll": "0.5", # seconds between buzzer checks (faster so we catch alarms)
"buzzer_cooldown": "10", # seconds to hold max brightness after buzzer stops
# lux mode boundaries (IHO S-52 three-mode framework)
"lux_night_max": "20", # 020 lux → NIGHT mode
"lux_dusk_max": "200", # 21200 lux → DUSK mode
"lux_day_dim_max": "500", # 201500 lux → DAY-DIM
"lux_day_normal_max": "1000", # 5011000 lux→ DAY-NORMAL
# >1000 lux → DAY-BRIGHT
# backlight levels per mode (1 = minimum, 5 = maximum, 0 is NEVER used)
"level_night": "1",
"level_dusk": "2",
"level_day_dim": "3",
"level_day_normal": "4",
"level_day_bright": "5",
"level_alert": "5", # during ALERT and COOLDOWN states
# hysteresis: require this many consecutive polls agreeing on a new ambient
# target before committing the change (prevents flickering near thresholds)
"hysteresis_polls": "3",
}
CONFIG_FILE = Path("/etc/screen-brightness.conf")
OVERRIDE_FILE = Path("/run/screen-brightness/override")
STATE_FILE = Path("/run/screen-brightness/state")
RUN_DIR = Path("/run/screen-brightness")
# ── Logging — stdout (captured by journald) + syslog ────────────────────────
log = logging.getLogger("screen-brightness")
log.setLevel(logging.INFO)
_fmt_detail = logging.Formatter("%(asctime)s [screen-brightness] %(levelname)s %(message)s")
_fmt_syslog = logging.Formatter("screen-brightness[%(process)d]: %(levelname)s %(message)s")
_stdout_h = logging.StreamHandler(sys.stdout)
_stdout_h.setFormatter(_fmt_detail)
log.addHandler(_stdout_h)
try:
_syslog_h = logging.handlers.SysLogHandler(address="/dev/log")
_syslog_h.setFormatter(_fmt_syslog)
log.addHandler(_syslog_h)
except OSError:
pass # /dev/log not available; journald captures stdout
# ── Config helpers ───────────────────────────────────────────────────────────
def load_config() -> dict:
cfg = dict(DEFAULTS)
if CONFIG_FILE.exists():
parser = configparser.ConfigParser()
parser.read(CONFIG_FILE)
section = "screen-brightness"
if parser.has_section(section):
cfg.update(parser[section])
log.info("Config loaded from %s", CONFIG_FILE)
return cfg
# ── Sysfs helpers ────────────────────────────────────────────────────────────
def _read_int(path: Path, default: Optional[int] = None) -> Optional[int]:
try:
return int(path.read_text().strip())
except Exception as exc:
log.warning("Cannot read %s: %s", path, exc)
return default
def _write_int(path: Path, value: int) -> bool:
try:
path.write_text(f"{value}\n")
return True
except Exception as exc:
log.error("Cannot write %s = %s: %s", path, value, exc)
return False
# ── Override file ────────────────────────────────────────────────────────────
def read_override() -> Optional[int]:
"""Return int level (1-5) if a manual override is active, else None."""
try:
if not OVERRIDE_FILE.exists():
return None
raw = OVERRIDE_FILE.read_text().strip().lower()
if raw in ("auto", ""):
return None
val = int(raw)
if 1 <= val <= 5:
return val
except Exception:
pass
return None
# ── State file (read by dashboard / other services) ──────────────────────────
def write_state(mode: str, level: int, lux: Optional[int],
buzzer: bool, override: bool) -> None:
try:
RUN_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(
f"mode={mode}\n"
f"level={level}\n"
f"lux={lux if lux is not None else 'N/A'}\n"
f"buzzer={'1' if buzzer else '0'}\n"
f"override={'1' if override else '0'}\n"
f"updated={int(time.time())}\n"
)
except Exception as exc:
log.debug("Cannot write state file: %s", exc)
# ── Lux → ambient level ───────────────────────────────────────────────────────
def lux_to_level(lux: int, cfg: dict) -> Tuple[int, str]:
"""Return (backlight_level, mode_name) for the given lux reading."""
if lux <= int(cfg["lux_night_max"]):
return int(cfg["level_night"]), "NIGHT"
if lux <= int(cfg["lux_dusk_max"]):
return int(cfg["level_dusk"]), "DUSK"
if lux <= int(cfg["lux_day_dim_max"]):
return int(cfg["level_day_dim"]), "DAY-DIM"
if lux <= int(cfg["lux_day_normal_max"]):
return int(cfg["level_day_normal"]), "DAY-NORMAL"
return int(cfg["level_day_bright"]), "DAY-BRIGHT"
# ── Main loop ────────────────────────────────────────────────────────────────
def main() -> None:
cfg = load_config()
BACKLIGHT = Path(cfg["backlight_path"])
BUZZER = Path(cfg["buzzer_path"])
LUX = Path(cfg["lux_path"])
for p in (BACKLIGHT, BUZZER, LUX):
if not p.exists():
log.warning("sysfs path not found at startup (will retry): %s", p)
RUN_DIR.mkdir(parents=True, exist_ok=True)
# SIGHUP → reload config without restart (e.g. systemctl reload)
_reload_flag = {"pending": False}
def _on_sighup(_sig: int, _frame: object) -> None:
_reload_flag["pending"] = True
signal.signal(signal.SIGHUP, _on_sighup)
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
poll_interval = float(cfg["poll_interval"])
log.info(
"Bridge brightness daemon started | "
"buzzer_poll=%ss lux_poll=%ss cooldown=%ss night≤%slux dusk≤%slux",
buzz_interval, poll_interval, cfg["buzzer_cooldown"],
cfg["lux_night_max"], cfg["lux_dusk_max"],
)
current_level: int = -1 # last level written to sysfs
cooldown_until: float = 0.0 # monotonic time when ALERT cooldown ends
buzzer_was_on: bool = False
# Hysteresis state — track how many consecutive polls agree on a new ambient target
pending_ambient: int = -1
pending_count: int = 0
# Buzzer is checked every buzzer_poll (0.5s); lux/ambient only every poll_interval (2s)
last_lux_poll: float = -999.0 # force first lux read
lux: Optional[int] = None # last successful lux read (reused between lux polls)
target, mode = 1, "NIGHT" # fallback for step calc before first ambient target
while True:
# ── Config reload on SIGHUP ──────────────────────────────────────────
if _reload_flag["pending"]:
cfg = load_config()
BACKLIGHT = Path(cfg["backlight_path"])
BUZZER = Path(cfg["buzzer_path"])
LUX = Path(cfg["lux_path"])
_reload_flag["pending"] = False
pending_ambient = -1
pending_count = 0
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
poll_interval = float(cfg["poll_interval"])
log.info("Config reloaded (SIGHUP)")
now = time.monotonic()
buzzer_raw = _read_int(BUZZER, default=0) or 0
buzzer = buzzer_raw > 0
override = read_override()
# Read lux only every poll_interval seconds
if now - last_lux_poll >= poll_interval:
last_lux_poll = now
lux = _read_int(LUX, default=None)
# ── Determine target level and mode name ─────────────────────────────
immediate = False # skip smooth stepping when True
if override is not None:
target, mode = override, "OVERRIDE"
immediate = True
elif buzzer:
target, mode = int(cfg["level_alert"]), "ALERT"
cooldown_until = now + int(cfg["buzzer_cooldown"])
immediate = True # alarm: go to max brightness instantly
if not buzzer_was_on:
log.info("ALERT: buzzer ON → brightness %d (immediate)", target)
elif now < cooldown_until:
target, mode = int(cfg["level_alert"]), "COOLDOWN"
immediate = True # hold alert level until cooldown expires
if buzzer_was_on:
remaining = cooldown_until - now
log.info("COOLDOWN: buzzer OFF → hold brightness %d for %.0fs",
target, remaining)
else:
# Ambient mode — lux controls level (use last lux read)
if lux is None:
# Sensor not read yet or unreadable — keep current level
buzzer_was_on = buzzer
time.sleep(buzz_interval)
continue
ambient_target, mode = lux_to_level(lux, cfg)
# Hysteresis: commit new ambient level only after N consecutive
# polls agree (avoids flickering near a lux threshold)
if ambient_target == pending_ambient:
pending_count += 1
else:
pending_ambient = ambient_target
pending_count = 1
if pending_count >= int(cfg["hysteresis_polls"]):
target = ambient_target
else:
# Not stable yet — keep current level if we have one
target = current_level if current_level > 0 else ambient_target
if buzzer_was_on and not buzzer and now >= cooldown_until:
log.info("COOLDOWN done → %s | lux=%d | brightness=%d", mode, lux, target)
# ── Smooth stepping vs immediate ─────────────────────────────────────
# Ambient changes step one level at a time to avoid harsh
# brightness jumps that could disrupt night-adapted vision.
# Alarm and override changes are immediate (safety requirement).
if immediate or current_level < 0:
step = target
elif target > current_level:
step = current_level + 1
elif target < current_level:
step = current_level - 1
else:
step = current_level # no change needed
# ── Write only when level actually changes ───────────────────────────
if step != current_level:
if _write_int(BACKLIGHT, step):
log.info(
"Brightness %s%d | mode=%-10s | lux=%s",
current_level if current_level >= 0 else "",
step, mode,
lux if lux is not None else "N/A",
)
current_level = step
write_state(mode, current_level, lux, buzzer, override is not None)
buzzer_was_on = buzzer
time.sleep(buzz_interval)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
log.info("Stopped by keyboard interrupt.")
sys.exit(0)

View File

@@ -0,0 +1,22 @@
[Unit]
Description=reTerminal DM screen brightness daemon (ambient + buzzer)
Documentation=https://wiki.seeedstudio.com/reterminal-dm/
After=multi-user.target
# Wait for the IIO light sensor and backlight sysfs nodes to be available
After=systemd-udev-settle.service
[Service]
Type=simple
# Ensure brightness starts in auto (ambient) mode on every boot
ExecStartPre=/bin/sh -c 'mkdir -p /run/screen-brightness && echo auto > /run/screen-brightness/override'
ExecStart=/usr/local/bin/screen-brightness.py
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
# Needs root to write to /sys/class/backlight and /sys/class/leds
User=root
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Step 14: Install screen brightness daemon (ambient light + buzzer-triggered alert)
# Regulatory: IMO MSC.191(79), MSC.302(87), IHO S-52 / IEC 62288
step_14_screen_brightness() {
local script_dst="/usr/local/bin/screen-brightness.py"
local service_dst="/etc/systemd/system/screen-brightness.service"
local conf_dst="/etc/screen-brightness.conf"
log "Downloading screen-brightness.py ..."
curl -fsSL "${FILE_SERVER}/screen-brightness.py" -o "$script_dst" \
|| { log "ERROR: could not download screen-brightness.py"; return 1; }
chmod 755 "$script_dst"
log "Installed $script_dst"
log "Downloading screen-brightness.service ..."
curl -fsSL "${FILE_SERVER}/screen-brightness.service" -o "$service_dst" \
|| { log "ERROR: could not download screen-brightness.service"; return 1; }
chmod 644 "$service_dst"
log "Installed $service_dst"
# Deploy default config only if one doesn't already exist on the device
# (preserves any vessel-specific tuning done post-provisioning)
if [[ ! -f "$conf_dst" ]]; then
log "Downloading screen-brightness.conf (default) ..."
curl -fsSL "${FILE_SERVER}/screen-brightness.conf" -o "$conf_dst" \
|| log "WARNING: could not download screen-brightness.conf (daemon uses built-in defaults)"
[[ -f "$conf_dst" ]] && chmod 644 "$conf_dst" && log "Installed $conf_dst"
else
log "Config $conf_dst already exists — keeping vessel settings"
fi
# Create the runtime dir; tmpfiles.d rule ensures it re-appears after reboot
mkdir -p /run/screen-brightness
cat > /etc/tmpfiles.d/screen-brightness.conf <<'TMPFILES'
d /run/screen-brightness 0755 root root -
TMPFILES
# Brightness API (localhost only) so overlay can set override without root
log "Downloading brightness-api.py ..."
curl -fsSL "${FILE_SERVER}/brightness-api.py" -o /usr/local/bin/brightness-api.py \
&& chmod 755 /usr/local/bin/brightness-api.py \
&& log "Installed /usr/local/bin/brightness-api.py" || true
if curl -fsSL "${FILE_SERVER}/brightness-api.service" -o /etc/systemd/system/brightness-api.service 2>/dev/null; then
chmod 644 /etc/systemd/system/brightness-api.service
systemctl enable brightness-api.service
log "brightness-api.service enabled"
fi
# Brightness overlay (crew manual control on top of kiosk)
if curl -fsSL "${FILE_SERVER}/brightness-overlay.py" -o "$PI_HOME/brightness-overlay.py" 2>/dev/null; then
chmod 755 "$PI_HOME/brightness-overlay.py"
chown "$PI_USER:$PI_USER" "$PI_HOME/brightness-overlay.py"
if curl -fsSL "${FILE_SERVER}/brightness-overlay-launch.sh" -o "$PI_HOME/brightness-overlay-launch.sh" 2>/dev/null; then
chmod 755 "$PI_HOME/brightness-overlay-launch.sh"
chown "$PI_USER:$PI_USER" "$PI_HOME/brightness-overlay-launch.sh"
fi
if curl -fsSL "${FILE_SERVER}/brightness-overlay.desktop" -o /tmp/br-overlay.desktop 2>/dev/null; then
sed "s|/home/pi|$PI_HOME|g" /tmp/br-overlay.desktop > "$AUTOSTART/brightness-overlay.desktop"
chown "$PI_USER:$PI_USER" "$AUTOSTART/brightness-overlay.desktop"
chmod 644 "$AUTOSTART/brightness-overlay.desktop"
rm -f /tmp/br-overlay.desktop
log "brightness-overlay installed (autostart)"
fi
fi
systemctl daemon-reload
systemctl enable screen-brightness.service
log "screen-brightness.service enabled (starts on next boot)"
}

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Test buzzer so screen-brightness daemon raises backlight (ALERT) and holds
# during COOLDOWN. Holds buzzer ON for 3+ seconds so the daemon's 2s poll sees it.
#
# reTerminal DM: /sys/class/leds/usr-buzzer/brightness (0 = off, 1 = on)
# Run as root: sudo ./test-buzzer-brightness.sh
set -e
BUZZER="/sys/class/leds/usr-buzzer/brightness"
MAX="/sys/class/leds/usr-buzzer/max_brightness"
if [[ ! -f "$BUZZER" ]]; then
echo "ERROR: Buzzer not found at $BUZZER" >&2
exit 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
echo "Run as root: sudo $0" >&2
exit 1
fi
# Quick check: is the daemon running? (so we know what to expect)
echo "--- Pre-check ---"
if systemctl is-active --quiet screen-brightness.service 2>/dev/null; then
echo " screen-brightness.service: active"
if [[ -f /run/screen-brightness/state ]]; then
echo " Current state:"
sed 's/^/ /' /run/screen-brightness/state
else
echo " (no /run/screen-brightness/state yet)"
fi
echo " Recent logs:"
journalctl -u screen-brightness.service -n 3 --no-pager 2>/dev/null | sed 's/^/ /' || true
else
echo " screen-brightness.service: NOT ACTIVE — start it with: sudo systemctl start screen-brightness.service"
echo " Brightness will not change until the daemon is running."
fi
echo ""
# Hold buzzer ON for 3 seconds so the daemon's 2s poll interval will see it
ON_VAL=255
if [[ -f "$MAX" ]]; then
ON_VAL="$(cat "$MAX" 2>/dev/null || echo 255)"
fi
echo "Beeping: 3 short beeps, then buzzer ON for 3 s (watch brightness go to 5)..."
for i in 1 2 3; do
echo "$ON_VAL" > "$BUZZER"
sleep 0.15
echo 0 > "$BUZZER"
sleep 0.25
done
echo "$ON_VAL" > "$BUZZER"
sleep 3
echo 0 > "$BUZZER"
echo "Buzzer off. Brightness should stay at 5 for ~10 s (cooldown), then return to ambient."
echo ""
echo "--- After (check state and logs) ---"
if [[ -f /run/screen-brightness/state ]]; then
echo " State:"
sed 's/^/ /' /run/screen-brightness/state
fi
echo " Recent logs:"
journalctl -u screen-brightness.service -n 8 --no-pager 2>/dev/null | sed 's/^/ /' || echo " (no logs or service not running)"

View File

@@ -48,7 +48,7 @@ if [[ -z "$FIRST_BOOT_CONF" ]]; then
fi fi
# Step enable flags (default: all enabled) # Step enable flags (default: all enabled)
for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13; do for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14; do
eval "ENABLE_STEP_${_n}=\"\${ENABLE_STEP_${_n}:-1}\"" eval "ENABLE_STEP_${_n}=\"\${ENABLE_STEP_${_n}:-1}\""
done done
@@ -165,6 +165,7 @@ elif [[ "$CURRENT_PHASE" == "2" ]]; then
run_step 10 cmdline run_step 10 cmdline
run_step 11 oneshots run_step 11 oneshots
run_step 12 log_permissions run_step 12 log_permissions
run_step 14 screen_brightness
phase2_cleanup phase2_cleanup
run_step 13 reboot run_step 13 reboot

View File

@@ -6,8 +6,9 @@
# 2. Copy this file to the boot partition as "user-data" (with meta-data and optional network-config). # 2. Copy this file to the boot partition as "user-data" (with meta-data and optional network-config).
# 3. Edit BOOTSTRAP_URL below to match your server (or set it once in the runcmd section). # 3. Edit BOOTSTRAP_URL below to match your server (or set it once in the runcmd section).
# #
# DNS: This config uses systemd-resolved; /etc/resolv.conf is a stub and DNS comes from DHCP # DNS: Use NetworkManager rc-manager=symlink so /etc/resolv.conf gets DHCP DNS (LXC option 6).
# (LXC option 6). Ensure bootstrap.sh does not overwrite /etc/resolv.conf. See docs/DEVICE-DNS-DHCP-RESOLVCONF.md. # RPi OS does not use systemd-resolved by default. Ensure bootstrap.sh does not overwrite
# /etc/resolv.conf. See docs/DEVICE-DNS-DHCP-RESOLVCONF.md.
package_update: true package_update: true
package_upgrade: false package_upgrade: false
@@ -15,7 +16,7 @@ package_upgrade: false
# Keep /etc/hosts in sync with hostname (from meta-data or set below) # Keep /etc/hosts in sync with hostname (from meta-data or set below)
manage_etc_hosts: true manage_etc_hosts: true
# DNS is managed by systemd-resolved; we do not overwrite /etc/resolv.conf # Do not overwrite /etc/resolv.conf; NetworkManager will manage it with DHCP DNS
manage_resolv_conf: false manage_resolv_conf: false
packages: packages:
@@ -28,76 +29,18 @@ write_files:
PasswordAuthentication yes PasswordAuthentication yes
PermitRootLogin no PermitRootLogin no
# Push current DHCP DNS into systemd-resolved (for dhcpcd/dhclient when NM doesn't feed resolved). # NetworkManager: manage resolv.conf via symlink so it gets DNS from DHCP (option 6 from LXC).
# With no args: discover DNS from lease or resolvectl and push to resolved for default IF. # RPi OS does not use systemd-resolved; NM writes /etc/resolv.conf -> /run/NetworkManager/resolv.conf.
# NetworkManager feeds resolved automatically; this covers first boot and non-NM setups. - path: /etc/NetworkManager/conf.d/99-resolv-dhcp.conf
- path: /usr/local/bin/update-resolv-from-dhcp.sh
content: |
#!/bin/sh
# Push DHCP DNS to systemd-resolved so resolv.conf (stub) uses it.
IF="${IFACE:-$(ip -o -4 route show to default 2>/dev/null | awk '{print $5}' | head -1)}"
[ -z "$IF" ] && exit 0
DNS=""
if [ -s /run/systemd/resolve/resolv.conf ]; then
DNS=$(grep -E '^nameserver\s+' /run/systemd/resolve/resolv.conf | awk '{print $2}' | tr '\n' ' ')
fi
if [ -z "$DNS" ]; then
DNS=$(resolvectl dns "$IF" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ')
fi
if [ -z "$DNS" ]; then
LEASE=$(ls /var/lib/dhcp/dhclient.*.leases 2>/dev/null | head -1)
[ -n "$LEASE" ] && DNS=$(grep -oP 'option domain-name-servers \K[^;]+' "$LEASE" 2>/dev/null | tr ',' '\n' | tr -d ' ' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ')
fi
[ -n "$DNS" ] && resolvectl dns "$IF" $DNS
permissions: '0755'
# dhclient: feed systemd-resolved on every lease acquire/renew (DHCP provides new_domain_name_servers)
- path: /etc/dhcp/dhclient-exit-hooks.d/zzz-update-resolv-conf
content: |
#!/bin/sh
# Run by dhclient on exit; push DHCP DNS into systemd-resolved.
[ -z "$new_domain_name_servers" ] && exit 0
[ -z "$interface" ] && exit 0
resolvectl dns "$interface" $new_domain_name_servers
permissions: '0755'
# NetworkManager: resolved is fed by NM by default; this only runs our script as fallback (e.g. if resolved started late).
- path: /etc/NetworkManager/dispatcher.d/99-update-resolv-from-dhcp
content: |
#!/bin/sh
[ "$2" = "up" ] || [ "$2" = "dhcp4-change" ] || exit 0
export IFACE="$1"
/usr/local/bin/update-resolv-from-dhcp.sh
permissions: '0755'
# Tell NetworkManager to send DHCP DNS to systemd-resolved (so every DHCP update is applied).
- path: /etc/NetworkManager/conf.d/99-use-resolved.conf
content: | content: |
[main] [main]
dns=systemd-resolved rc-manager=symlink
rc-manager=unmanaged permissions: '0644'
# Fallback: push DHCP DNS to resolved once when network is up (e.g. dhcpcd-only or first boot).
- path: /etc/systemd/system/update-resolv-from-dhcp.service
content: |
[Unit]
Description=Push DHCP DNS to systemd-resolved
After=network-online.target systemd-resolved.service
WantedBy=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-resolv-from-dhcp.sh
RemainAfterExit=yes
runcmd: runcmd:
# Use systemd-resolved for DNS; /etc/resolv.conf -> stub so all lookups go through resolved (DHCP DNS applied by NM/hooks). # Remove static resolv.conf so NM creates its symlink with DHCP DNS (file.server will resolve).
- systemctl enable systemd-resolved.service - rm -f /etc/resolv.conf
- systemctl start systemd-resolved.service - systemctl restart NetworkManager || true
- rm -f /etc/resolv.conf && ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# Push current DHCP DNS into resolved once at first boot (in case NM hasn't applied yet).
- /usr/local/bin/update-resolv-from-dhcp.sh
- systemctl enable update-resolv-from-dhcp.service
- systemctl enable ssh - systemctl enable ssh
- systemctl start ssh - systemctl start ssh
# Download and run bootstrap script (edit URL to match your file server) # Download and run bootstrap script (edit URL to match your file server)

View File

@@ -1,33 +1,86 @@
# Deploy CM4 eMMC Provisioning to a New Proxmox Instance # Deploying the CM4 eMMC Provisioning Stack to Proxmox
Step-by-step guide to deploy the provisioning service (host + LXC) on a **new** Proxmox server. For redeploy/update and troubleshooting, see [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md). Complete step-by-step guide for deploying the provisioning service (Proxmox host + LXC container) on a new or existing Proxmox server. Covers host preparation, network bridge configuration, LXC deployment, post-deploy setup, network boot, and first-boot asset sync.
For reference details (troubleshooting, redeploy, architecture), see [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
--- ---
## Prerequisites (before running the deploy script) ## Overview
| Requirement | Details | The provisioning stack consists of two parts that work together:
|-------------|---------|
| **Proxmox host** | A Proxmox VE node (new or existing) where you want the service. |
| **SSH as root** | You must be able to run `ssh root@YOUR_PROXMOX_HOST` with **key-based auth** (no password prompt). |
| **Proxmox storage** | At least one active storage (e.g. `local` or `local-lvm`). Check on the host: `pvesm status`. |
| **Host internet** (recommended) | Needed so the deploy script can download the Debian 12 LXC template (if missing), and install **usbboot** and **PiShrink** on the host. Without internet, deploy still runs but you must install usbboot and PiShrink manually later. |
**Optional (set before deploy):** | Component | Where it runs | What it does |
|-----------|--------------|--------------|
| **Host scripts + udev** | Proxmox host | Detects CM4 over USB, runs `rpiboot`, then `dd` to write/read the eMMC |
| **LXC container** (`cm4-provisioning`) | Proxmox LXC | Runs the Flask dashboard on port 5000; serves portal files and golden images |
- `DEPLOY_ROOTFS_STORAGE=local-lvm` — Skip interactive storage choice when creating the LXC. The host and LXC share `/var/lib/cm4-provisioning/` via a bind-mount, so images and status files are visible from both.
- `DEPLOY_LXC_ROOT_PASSWORD=yourpassword` — Set LXC root password and enable SSH.
- `DEPLOY_LXC_SSH_KEY=/path/to/pub` — Copy this key into the LXC (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub`). ```
- `CM4_BACKUPS_HOST_PATH=/mnt/storage/cm4-backups` — Store backups on this host path (create the directory on the host if needed). Workstation (this repo)
- **Network (WAN/LAN):**
`DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default), │ deploy-to-proxmox.sh (SSH + rsync)
`DEPLOY_LXC_LAN_BRIDGE=vmbr1`, `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` — To add eth1 as provisioning LAN. **Set these if you want the portal reachable from the LAN** (e.g. http://10.20.50.1:5000); the dashboard listens on all interfaces.
Proxmox Host
├── udev → cm4-flash-trigger.sh → flash-emmc-on-connect.sh
├── /opt/cm4-provisioning/ (scripts, env)
├── /var/lib/cm4-provisioning/ (golden.img, backups, status.json) ◄──────┐
└── LXC: cm4-provisioning │ bind-mount
├── Flask dashboard :5000 │
├── /opt/cm4-provisioning/dashboard/ │
└── /var/lib/cm4-provisioning/ ◄────────────────────────────────────┘
```
--- ---
## Step 1: Run the deploy script ## Part 1 — Proxmox Host Prerequisites
From your **workstation** (where the repo is cloned), run: ### 1.1 Hardware & OS
- Proxmox VE 7 or 8 installed on a physical machine.
- At least one USB port accessible to the host (not passed through to a VM) for the reTerminal USB slave cable.
- At least one active storage (local or local-lvm). Check: `pvesm status`.
- Internet access on the host (needed for initial usbboot/PiShrink install and LXC template download).
### 1.2 SSH key access from your workstation
The deploy script connects as `root` using key-based auth. Set this up if not already done:
```bash
# On your workstation — copy your public key to the Proxmox host
ssh-copy-id root@YOUR_PROXMOX_HOST
# Verify
ssh root@YOUR_PROXMOX_HOST "echo OK"
```
### 1.3 Proxmox network bridges
The LXC needs at minimum one bridge for WAN access. For provisioning LAN (DHCP to devices), it needs a second bridge.
#### WAN bridge (required)
The default WAN bridge is `vmbr0`, which is created automatically by Proxmox during installation and connects to your primary network. No extra configuration needed.
#### LAN bridge for provisioning (required for network boot / device DHCP)
If you want the LXC to serve DHCP, TFTP, and DNS on a dedicated provisioning LAN (so devices can network-boot), create a second Linux bridge on the Proxmox host:
1. Open **Proxmox Web UI → Node → Network → Create → Linux Bridge**.
2. Set:
- **Name:** `vmbr1` (or any unused bridge name)
- **Bridge ports:** the physical NIC connected to your provisioning LAN switch (e.g. `enp2s0`). Leave blank for an internal-only bridge (useful for testing with no physical switch).
- **IPv4/CIDR:** leave blank (the LXC handles the IP on this bridge, not the host).
- **Autostart:** checked.
3. Click **Create**, then **Apply Configuration**.
> **Note:** If you connect the reTerminals via a switch that is also connected to `enp2s0`, traffic flows directly. If there is no physical NIC to dedicate, leave bridge ports blank and connect all provisioning devices as VMs/LXCs on the same internal bridge.
---
## Part 2 — Running the Deploy Script
From your **workstation** (where this repo is cloned), run a single command to deploy everything:
```bash ```bash
cd /path/to/reTerminal\ DM4 cd /path/to/reTerminal\ DM4
@@ -35,9 +88,26 @@ cd /path/to/reTerminal\ DM4
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST ./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
``` ```
Replace `YOUR_PROXMOX_HOST` with the Proxmox hostname or IP (e.g. `10.20.30.40`). Replace `YOUR_PROXMOX_HOST` with the Proxmox hostname or IP address.
**Example with options:** ### 2.1 Deploy script environment variables
Set these before running the script to customise the deployment:
| Variable | Default | Description |
|----------|---------|-------------|
| `DEPLOY_ROOTFS_STORAGE` | *(interactive)* | LXC rootfs storage name (e.g. `local-lvm`). If not set, script lists storages and asks. |
| `DEPLOY_LXC_WAN_BRIDGE` | `vmbr0` | Proxmox bridge for WAN (eth0 in LXC). |
| `DEPLOY_LXC_WAN_IP` | `dhcp` | WAN address: `dhcp` or a static IP like `192.168.1.10/24`. |
| `DEPLOY_LXC_LAN_BRIDGE` | *(none)* | If set, adds eth1 as provisioning LAN on this bridge (e.g. `vmbr1`). |
| `DEPLOY_LXC_LAN_SUBNET` | `10.20.50.1/24` | LXC IP/prefix on the LAN bridge. Used only when `DEPLOY_LXC_LAN_BRIDGE` is set. |
| `DEPLOY_LXC_ROOT_PASSWORD` | *(default)* | Sets LXC root password and enables SSH inside the container. |
| `DEPLOY_LXC_SSH_KEY` | `~/.ssh/id_ed25519.pub` | Public key to add to LXC root's `authorized_keys`. Defaults to your workstation key. |
| `CM4_BACKUPS_HOST_PATH` | *(none)* | Host directory for backup images (e.g. `/mnt/storage/cm4-backups`). Bind-mounted into LXC. |
| `DEPLOY_EMMC_SIZE_GB` | `32` | eMMC size hint in GB (used only when multiple block devices appear after rpiboot). |
| `DEPLOY_LOG` | *(off)* | Set to `1` to write a timestamped log file in `scripts/`. |
### 2.2 Full deploy example
```bash ```bash
DEPLOY_ROOTFS_STORAGE=local-lvm \ DEPLOY_ROOTFS_STORAGE=local-lvm \
@@ -47,117 +117,352 @@ DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 \
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.20.30.40 ./emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.20.30.40
``` ```
- On **first run**, the script will ask you to choose LXC rootfs storage (unless `DEPLOY_ROOTFS_STORAGE` is set). It then creates the LXC, installs host scripts, udev, systemd units, and the dashboard in the LXC. ### 2.3 What the deploy script does
- The script prints **LXC IP (WAN)** and, if you set `DEPLOY_LXC_LAN_BRIDGE`, **LXC IP (LAN)**. The portal is reachable at `http://<IP>:5000` on both; use the LAN IP from devices on the provisioning LAN.
The script runs five stages:
1. **Check** — SSHes to the host, finds existing `cm4-provisioning` container by hostname (or lists storage for new container creation).
2. **Clean + Rsync** — Wipes `/tmp/emmc-provisioning-deploy` on the host and rsyncs the entire repo there (excluding `.git` and deploy logs).
3. **Remote install (host + LXC)** — Runs a remote heredoc that:
- Creates the LXC (Debian 12, 1 GB RAM, 8 GB rootfs) if it doesn't exist, or reuses it by hostname.
- Adds eth1 (LAN bridge) if `DEPLOY_LXC_LAN_BRIDGE` is set.
- Configures the bind-mount for `/var/lib/cm4-provisioning/`.
- Installs host scripts to `/opt/cm4-provisioning/` and udev rules to `/etc/udev/rules.d/`.
- Installs and enables systemd units: `cm4-flash.service`, `cm4-build-cloudinit.path/.service`, `cm4-shrink.path/.service`.
- Writes `/opt/cm4-provisioning/env` (golden image path, rpiboot dir, eMMC size).
- Installs `python3-flask` and `openssh-server` in the LXC (skipped if already present).
- Deploys the Flask dashboard and enables/restarts `cm4-dashboard.service` in the LXC.
- Installs usbboot (`rpiboot`) on the host if not already present.
- Installs PiShrink on the host if not already present.
4. **LXC start** — Starts the LXC if stopped.
5. **Summary** — Prints LXC WAN IP (and LAN IP if set), dashboard URL, and remaining manual steps.
On **redeploy** (container already exists): host scripts, dashboard, env, systemd, and udev are always updated. LXC creation, bind-mounts, apt installs, usbboot, and PiShrink are skipped when already present.
--- ---
## Step 2: Install usbboot on the host (if host had no internet during deploy) ## Part 3 — Post-Deploy: Required Manual Steps
USB flash/backup needs **rpiboot** on the Proxmox **host**. If the deploy log said usbboot install failed or was skipped: ### Step 1: Verify the dashboard is up
**From your workstation:**
```bash ```bash
scp emmc-provisioning/scripts/install-usbboot-on-host.sh root@YOUR_PROXMOX_HOST:/tmp/ # Get the LXC IP from deploy output, or:
ssh root@YOUR_PROXMOX_HOST "bash /tmp/install-usbboot-on-host.sh" ssh root@YOUR_PROXMOX_HOST \
"CID=\$(pct list | awk '\$3==\"cm4-provisioning\"{print \$1}'); pct exec \$CID -- hostname -I"
# Open the dashboard
open http://<LXC-IP>:5000
``` ```
**Or on the Proxmox host** (if `/tmp/emmc-provisioning-deploy` is still there): The dashboard should show **"Waiting for device in USB boot mode"** on the home page.
### Step 2: Verify host services
SSH to the Proxmox host and confirm the host side is healthy:
```bash ```bash
ssh root@YOUR_PROXMOX_HOST ssh root@YOUR_PROXMOX_HOST
bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
# Check udev rule is installed
ls /etc/udev/rules.d/90-cm4-boot-mode.rules
# Check flash trigger script
ls /usr/local/bin/cm4-flash-trigger.sh
# Check host scripts
ls /opt/cm4-provisioning/
# Expected: flash-emmc-on-connect.sh, build-cloudinit-image.sh,
# run-shrink-on-host.sh, fix-gadget-bootcode-on-host.sh, env
# Check systemd path units are active
systemctl status cm4-build-cloudinit.path cm4-shrink.path
# Check auto-flash is enabled
ls /etc/cm4-provisioning/enabled
``` ```
--- ### Step 3: Add a golden image
## Step 3: Add a golden image (required for Deploy) A **golden image** is required for **Deploy** (writing an image to the device's eMMC). Backup (reading from device) works without it.
To **write** an image to a device (Deploy), the host must have a **golden image** at `/var/lib/cm4-provisioning/golden.img`. Backup (read from device) works without it. **Option A — Build via the dashboard:**
1. Open `http://<LXC-IP>:5000` → Admin tab.
**Option A — From the dashboard** 2. Click **Build cloud-init image**: the host downloads the latest Raspberry Pi OS, injects your cloud-init `user-data`, and creates `golden.img`.
3. Click **Set as golden** once the build finishes.
1. Open **http://&lt;LXC-IP&gt;:5000** (use the LXC IP from the deploy output).
2. Build a cloud-init image or upload/set an existing backup as golden (see dashboard Admin).
**Option B — Copy an image from your machine**
**Option B — Copy an existing image:**
```bash ```bash
scp /path/to/your-golden.img root@YOUR_PROXMOX_HOST:/var/lib/cm4-provisioning/golden.img scp /path/to/your-golden.img root@YOUR_PROXMOX_HOST:/var/lib/cm4-provisioning/golden.img
``` ```
--- **Option C — Promote a backup:**
In the dashboard Admin → Images tab, select a backup and click **Set as golden**.
## Accessing the portal from the LAN ### Step 4: Enable SSH into the LXC (optional)
The dashboard listens on **all interfaces** (`0.0.0.0:5000`), so it is reachable on both WAN and LAN IPs when the LXC has two networks. If you ran the deploy with `DEPLOY_LXC_ROOT_PASSWORD` or a default SSH key, the LXC already has SSH enabled. Otherwise:
- **Deploy with a LAN interface:** set `DEPLOY_LXC_LAN_BRIDGE=vmbr1` (and optionally `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24`) when running the deploy script. The LXC will get eth1 with the LAN IP (e.g. 10.20.50.1). ```bash
- **From the provisioning LAN:** open **http://&lt;LAN-IP&gt;:5000** (e.g. http://10.20.50.1:5000). Devices on that subnet can use the portal without going through WAN. # From your workstation — adds your default SSH key and enables root SSH
- If you did not set a LAN bridge at deploy time, you only have one IP (WAN); use that for the portal. To add LAN later you would need to add eth1 to the container and reconfigure (see PROXMOX-LXC-DEPLOYMENT.md). ./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST
--- # Or with a specific key and password
ROOT_PASSWORD='YourPassword' \
## Step 4: (Optional) SSH into the LXC ./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST ~/.ssh/id_ed25519.pub
```
If you set `DEPLOY_LXC_ROOT_PASSWORD` or had a default SSH key, you can already run:
Then connect:
```bash ```bash
ssh root@<LXC-IP> ssh root@<LXC-IP>
``` ```
Otherwise, enable root SSH and add your key: ---
## Part 4 — Sync Portal Files (First-Boot Assets)
The LXC serves first-boot assets (kiosk scripts, desktop files, splash, theme, etc.) from `/var/lib/cm4-provisioning/portal-files/`. These must be synced from the repo.
```bash ```bash
./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST # From your workstation
# Or with password: ROOT_PASSWORD='YourPassword' ./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST ~/.ssh/id_ed25519.pub ./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>
``` ```
This rsyncs everything under `emmc-provisioning/cloud-init/fileserver/` to the LXC portal-files directory. Run this every time you update kiosk assets or first-boot scripts.
**What gets synced:**
| File | Purpose |
|------|---------|
| `start-chromium.sh` | Wayland/labwc Chromium kiosk launcher |
| `five-tap-close-chromium.py` | 5-tap touch overlay to close Chromium |
| `chromium-kiosk.desktop` | Autostart: launches Chromium kiosk |
| `chromium-kiosk-no-select/` | Chromium extension: disables text selection |
| `set-rotation-at-login.sh/.desktop` | Per-login screen rotation |
| `01-set-rotation-once.sh/.desktop` | One-shot: rotation + dark theme + kanshi |
| `02-set-wallpaper-once.sh/.desktop` | One-shot: set wallpaper |
| `99-default-session.conf` | LightDM session = `rpd-labwc` |
| `custom.plymouth` + `custom.script` | Plymouth boot splash theme |
| `splash.png` | Boot splash / wallpaper image |
| `steps/0113*.sh` | First-boot step scripts sourced by `first-boot.sh` |
--- ---
## Step 5: (Optional) Network boot (DHCP + TFTP on eth1) ## Part 5 — Network Boot Setup (Optional)
Only if you deployed with **`DEPLOY_LXC_LAN_BRIDGE`** (and optionally `DEPLOY_LXC_LAN_SUBNET`) and want to offer network boot to devices on that LAN: Only needed if you want devices to **boot over the network** (PXE-style via TFTP) for provisioning, rather than via USB cable.
### 5.1 Prerequisites
- The LXC must have been deployed with a **LAN bridge** (`DEPLOY_LXC_LAN_BRIDGE` set). The LXC's eth1 will be the provisioning LAN gateway.
- Devices must be connected to the same LAN as the LXC's eth1.
### 5.2 Run the network boot setup script
From your workstation:
```bash ```bash
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP> ./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP>
``` ```
See [NETWORK-BOOT-LXC.md](NETWORK-BOOT-LXC.md) for details. This SSH-connects to the LXC and runs the full setup inside the container. It performs:
1. **Installs dnsmasq** (DHCP + DNS server) and the `vlan` package (for VLAN interfaces).
2. **Configures dnsmasq** on eth1:
- DHCP range: `<LAN_BASE>.100` `<LAN_BASE>.200` (e.g. `10.20.50.100``10.20.50.200`).
- DNS: static record `file.server` → LAN gateway IP, so first-boot scripts can reach `http://file.server/...`.
- DHCP option 6: sends LXC as DNS server to all DHCP clients.
3. **Configures extra IPs on eth1**: `192.168.30.1/24`, `192.168.127.1/24` (for serving multiple subnets).
4. **Creates VLAN 40** interface `eth1.40` at `192.168.0.1/24` (for VLAN-tagged networks on the provisioning LAN).
5. **Enables IP forwarding** (`net.ipv4.ip_forward=1`) persisted in `/etc/sysctl.d/`.
6. **Configures NAT** (nftables or iptables fallback): masquerades all LAN traffic out eth0 so devices on the provisioning LAN get internet access.
7. **Enables and starts dnsmasq**.
Config files written:
- `/etc/dnsmasq.d/network-boot.conf` — DHCP + DNS on eth1
- `/etc/nftables.d/nat-lan.conf` — NAT rules
- `/etc/network/interfaces.d/70-cm4-extra-lan` — extra IPs and VLAN persisted
### 5.3 Enable PXE / TFTP network boot
TFTP is not enabled by default (dnsmasq is configured for DHCP + DNS only). To enable PXE/TFTP so devices can load a kernel and initramfs over the network:
```bash
# SSH into the LXC
ssh root@<LXC-IP>
# Enable PXE/TFTP (adds the PXE options to dnsmasq and restarts it)
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable
```
This activates the PXE snippet at `/etc/dnsmasq.d/network-boot-pxe.conf` (DHCP options 66/67: next-server + boot file) and reloads dnsmasq.
To disable PXE again (keep DHCP/DNS only):
```bash
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh disable
```
### 5.4 Populate the TFTP boot files
The TFTP root (`/srv/tftpboot`) needs Raspberry Pi 4 / CM4 boot files. From your workstation:
```bash
./emmc-provisioning/scripts/populate-tftpboot-from-git.sh root@<LXC-IP>
```
This downloads the official Raspberry Pi firmware `boot/` folder from GitHub into `/srv/tftpboot` on the LXC.
To add the custom provisioning initramfs (Alpine-based, allows Backup/Deploy from network boot):
```bash
# Ensure the initramfs image is built (or use the pre-built one in the repo)
ls emmc-provisioning/network-boot-initramfs/initrd.img
# Copy it to the LXC TFTP root
scp emmc-provisioning/network-boot-initramfs/initrd.img root@<LXC-IP>:/srv/tftpboot/
# Then ensure config.txt references it
./emmc-provisioning/scripts/ensure-tftpboot-config-kernel-initrd.sh root@<LXC-IP>
```
### 5.5 Configure device EEPROM for network boot
For a reTerminal to boot from the network (when eMMC is empty or network boot order is set), its EEPROM `BOOT_ORDER` must include network boot. The recommended order is `0xf21` (eMMC first, then network):
```bash
# Check current EEPROM boot order on a connected device
./emmc-provisioning/scripts/check-network-boot-priority.sh root@<DEVICE-IP>
```
To set boot order via the provisioning dashboard (when device is in USB boot mode), use the **Update EEPROM** button in the dashboard.
### 5.6 Verify network boot is working
On the LXC, check the following:
```bash
# Is dnsmasq running?
systemctl status dnsmasq
# Is TFTP/PXE enabled?
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh status
# Are TFTP boot files present?
ls /srv/tftpboot/start4cd.elf
# Any DHCP leases from devices?
cat /var/lib/misc/dnsmasq.leases
# Monitor live DHCP/TFTP traffic when powering on a device
tcpdump -i eth1 -n port 67 or port 68 or port 69
```
--- ---
## Step 6: (Optional) Install PiShrink on the host ## Part 6 — Installing usbboot Manually (if needed)
If the deploy log said PiShrink install failed (e.g. no internet), and you want **Shrink/Compress** in the dashboard to work: If the deploy script could not install usbboot (e.g. no internet during deploy), install it manually:
```bash
# From your workstation
scp emmc-provisioning/scripts/install-usbboot-on-host.sh root@YOUR_PROXMOX_HOST:/tmp/
ssh root@YOUR_PROXMOX_HOST "bash /tmp/install-usbboot-on-host.sh"
```
Or, if `/tmp/emmc-provisioning-deploy` is still on the host:
```bash
ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh"
```
After install, verify:
```bash
ssh root@YOUR_PROXMOX_HOST "ls /opt/usbboot/rpiboot && ls /opt/usbboot/mass-storage-gadget64/"
```
---
## Part 7 — Installing PiShrink Manually (if needed)
PiShrink enables the dashboard **Shrink/Compress** function (shrinks backup images before compressing). Install if the deploy failed:
```bash ```bash
ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh" ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh"
# Or stream from workstation:
ssh root@YOUR_PROXMOX_HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh
``` ```
Or from your machine (stream the script): use the same pattern as in [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md) for `install-pishrink-on-host.sh`.
--- ---
## Summary checklist ## Part 8 — Updating / Redeploying
| Step | Action | Required? | To push code changes (scripts, dashboard, udev, systemd) to an existing deployment:
|------|--------|------------|
| 1 | Run `deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST` | **Yes** |
| 2 | Install usbboot on host (if deploy couldnt) | For USB flash/backup |
| 3 | Add `golden.img` for Deploy | For Deploy only |
| 4 | SSH to LXC (or use setup-lxc-ssh.sh) | Optional |
| 5 | Run setup-network-boot-on-lxc.sh (if using eth1 LAN) | Optional |
| 6 | Install PiShrink on host (if deploy couldnt) | For Shrink/Compress |
**After deployment:** ```bash
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
```
- **Dashboard:** http://&lt;LXC-IP&gt;:5000 (WAN). If you set `DEPLOY_LXC_LAN_BRIDGE`, also **http://&lt;LAN-IP&gt;:5000** (e.g. http://10.20.50.1:5000) from the LAN. The script finds the container by hostname (`cm4-provisioning`) and updates all files. It does **not** overwrite your `golden.img` or `/etc/cm4-provisioning/enabled`.
- **Golden image path (host and LXC):** `/var/lib/cm4-provisioning/golden.img`
- **Disable auto-flash:** `ssh root@YOUR_PROXMOX_HOST "rm /etc/cm4-provisioning/enabled"`
- **Enable again:** `ssh root@YOUR_PROXMOX_HOST "touch /etc/cm4-provisioning/enabled"`
**If you see "rpiboot failed or no device connected":** The error is from the **Proxmox host** (where USB is connected). On the host run: `tail -50 /var/lib/cm4-provisioning/flash.log` to see the real rpiboot message. Ensure the reTerminal is in **boot mode** (eMMC disable jumper, USB slave port), then unplug/replug. See [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md) § "If rpiboot fails" for full steps. To update **only the dashboard** (faster when only `app.py` or templates changed):
Full reference: [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md). ```bash
./emmc-provisioning/scripts/deploy-dashboard-to-lxc.sh root@<LXC-IP>
```
To update **only the portal files** (kiosk assets, first-boot scripts):
```bash
./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>
```
---
## Deployment Checklist
| # | Action | Script / Command | Required? |
|---|--------|-----------------|-----------|
| **Host prep** | | | |
| 1 | SSH key access to Proxmox host | `ssh-copy-id root@HOST` | **Yes** |
| 2 | Create LAN bridge on Proxmox (`vmbr1`) | Proxmox Web UI | For network boot |
| **Deploy** | | | |
| 3 | Run deploy script | `deploy-to-proxmox.sh root@HOST` | **Yes** |
| 4 | Verify dashboard is up | `http://<LXC-IP>:5000` | **Yes** |
| 5 | Verify host services and udev rule | `ssh root@HOST "ls /etc/udev/rules.d/90-cm4-boot-mode.rules"` | **Yes** |
| **Post-deploy** | | | |
| 6 | Add golden image for Deploy | Dashboard Admin or `scp golden.img root@HOST:/var/lib/cm4-provisioning/` | For Deploy |
| 7 | Sync portal files (kiosk/first-boot assets) | `sync-portal-files-to-lxc.sh root@<LXC-IP>` | For first-boot provisioning |
| 8 | Enable SSH into LXC | `setup-lxc-ssh.sh root@HOST` | Optional |
| **Network boot** | | | |
| 9 | Run network boot setup on LXC | `setup-network-boot-on-lxc.sh root@<LXC-IP>` | For network boot only |
| 10 | Enable PXE/TFTP | `ssh root@<LXC-IP> /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable` | For PXE boot |
| 11 | Populate TFTP boot files | `populate-tftpboot-from-git.sh root@<LXC-IP>` | For PXE boot |
| 12 | Copy provisioning initramfs to TFTP | `scp network-boot-initramfs/initrd.img root@<LXC-IP>:/srv/tftpboot/` | For provisioning via netboot |
| 13 | Configure device EEPROM boot order | Dashboard **Update EEPROM** or `rpi-eeprom-config` | For network boot |
| **If needed** | | | |
| 14 | Install usbboot manually | `install-usbboot-on-host.sh` | If deploy had no internet |
| 15 | Install PiShrink manually | `install-pishrink-on-host.sh` | If deploy had no internet |
---
## After Deployment: Quick Reference
| What | How |
|------|-----|
| **Dashboard (WAN)** | `http://<LXC-IP>:5000` |
| **Dashboard (LAN)** | `http://10.20.50.1:5000` (if LAN bridge was set) |
| **SSH to LXC** | `ssh root@<LXC-IP>` |
| **Get LXC IP** | `ssh root@HOST "pct list; pct exec <CTID> -- hostname -I"` |
| **Golden image path** | `/var/lib/cm4-provisioning/golden.img` (same on host and in LXC) |
| **Disable auto-flash** | `ssh root@HOST "rm /etc/cm4-provisioning/enabled"` |
| **Re-enable auto-flash** | `ssh root@HOST "touch /etc/cm4-provisioning/enabled"` |
| **Flash log (on host)** | `ssh root@HOST "tail -f /var/lib/cm4-provisioning/flash.log"` |
| **Status JSON (on host)** | `ssh root@HOST "cat /var/lib/cm4-provisioning/status.json"` |
| **Full host snapshot** | `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/monitor-from-host.sh` |
| **DHCP leases (LXC)** | `ssh root@<LXC-IP> "cat /var/lib/misc/dnsmasq.leases"` |
---
## Troubleshooting
For USB flash errors (rpiboot failures, block device not found, USB transfer errors) and LXC/dashboard issues, see the full troubleshooting section in [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
For network boot issues (DHCP not working, device not appearing in dashboard), see [NETWORK-BOOT-TROUBLESHOOTING.md](NETWORK-BOOT-TROUBLESHOOTING.md).

View File

@@ -20,13 +20,13 @@ This document describes how to configure provisioned devices (e.g. Raspberry Pi
## What to change in cloud-init ## What to change in cloud-init
### Option A: user-data.bootstrap (systemd-resolved) ### Option A: user-data.bootstrap (uses Option B for RPi OS)
**File:** `cloud-init/user-data.bootstrap` **File:** `cloud-init/user-data.bootstrap`
- **manage_resolv_conf: false** — already set; cloud-init must not overwrite resolv.conf. - **manage_resolv_conf: false** — already set; cloud-init must not overwrite resolv.conf.
- **systemd-resolved** — runcmd enables/starts resolved and makes `/etc/resolv.conf` a symlink to `stub-resolv.conf`. Resolved gets DNS from NetworkManager (and from the hooks in write_files). - **NetworkManager** — `99-resolv-dhcp.conf` has `rc-manager=symlink` so NM creates `/etc/resolv.conf` with DHCP DNS. RPi OS does not use systemd-resolved by default.
- **NetworkManager** — `99-use-resolved.conf` has `dns=systemd-resolved` and `rc-manager=unmanaged` so NM doesnt write resolv.conf; resolved does. - **runcmd** — removes static resolv.conf and restarts NM so it creates the symlink with DHCP option 6.
- **Bootstrap script** — must **not** write `nameserver 8.8.8.8` (or any fixed server) into `/etc/resolv.conf`. Our `bootstrap.sh` no longer does that. - **Bootstrap script** — must **not** write `nameserver 8.8.8.8` (or any fixed server) into `/etc/resolv.conf`. Our `bootstrap.sh` no longer does that.
No extra changes needed if you use `user-data.bootstrap` as-is; just ensure your bootstrap script does not touch resolv.conf. No extra changes needed if you use `user-data.bootstrap` as-is; just ensure your bootstrap script does not touch resolv.conf.

View File

@@ -0,0 +1,280 @@
# Deploying Lite + Wayland Kiosk on reTerminal DM
This guide describes how to deploy a **minimal kiosk** on the reTerminal DM (Raspberry Pi CM4-based) using **Raspberry Pi OS Lite** plus a **Wayland compositor** (no full PIXEL desktop). The result is a device that boots to **Chromium in kiosk mode** with your **brightness overlay** on top; if Chromium exits, you can relaunch it so the user has no other UI to access.
**See also:**
- [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) — brightness daemon, API, and overlay installation.
- [SCREEN-BRIGHTNESS-SUMMARY.md](SCREEN-BRIGHTNESS-SUMMARY.md) — overview of the brightness system.
---
## 1. Overview
| Component | Role |
|-----------|------|
| **Raspberry Pi OS Lite** | Base OS; no PIXEL, no full desktop. |
| **Wayland compositor (Weston)** | Minimal display server; runs Chromium and the brightness overlay. |
| **Chromium** | Kiosk browser (fullscreen, single URL). |
| **screen-brightness + brightness-api** | System services; backlight and HTTP API (unchanged). |
| **brightness-overlay** | GTK + GtkLayerShell; stays on top of Chromium on Wayland. |
Benefits of Lite + Wayland (vs Lite + X11):
- **Brightness overlay** uses **GtkLayerShell** natively, so it stays above fullscreen Chromium without window-manager hacks.
- No full desktop; fewer packages and a smaller attack surface.
- Waylands input and security model.
---
## 2. Prerequisites
- **Hardware:** reTerminal DM (CM4) with display, and (for brightness) backlight, buzzer, and light sensor (see [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md#2-verify-hardware)).
- **Base image:** Raspberry Pi OS **Lite** (64-bit recommended). Either:
- Flash **Raspberry Pi OS Lite** from [raspberrypi.com/software](https://www.raspberrypi.com/software/), or
- Start from an existing full OS and remove the desktop (more work; starting from Lite is simpler).
- **Network:** Device can reach the internet (or your mirror) for `apt`.
- **reTerminal DM drivers:** Installed so `/sys/class/backlight/lcd_backlight` and IIO light sensor exist (Seeed [reTerminal DM wiki](https://wiki.seeedstudio.com/reterminal-dm/)).
---
## 3. Install base system (Lite)
If you are not already on Lite:
1. Download **Raspberry Pi OS Lite** (e.g. 64-bit) and write it to the eMMC/SD as usual.
2. Boot, expand filesystem if needed, set hostname/locale, and ensure the reTerminal DM kernel/drivers are installed (e.g. from Seeeds repo or your image).
3. (Optional) Configure cloud-init or your first-boot flow so new images get the same base.
---
## 4. Install Wayland and Weston
On the device (or in your image build):
```bash
sudo apt-get update
sudo apt-get install -y \
weston \
chromium-browser \
xwayland
```
- **weston** — Reference Wayland compositor; suitable for kiosks and supports layer-shell (used by the brightness overlay).
- **chromium-browser** — Use the Raspberry Pi OS / Debian package so it runs natively on Wayland when `WAYLAND_DISPLAY` is set.
- **xwayland** — Optional; only needed if you have X11-only apps.
Verify Weston runs (for a quick test, start a temporary session):
```bash
weston --tty=1
# You should see a Weston desktop. Exit with Ctrl+Alt+Backspace or by killing weston.
```
---
## 5. Install brightness stack (daemon + API + overlay)
Install the screen-brightness daemon and the brightness API as **system services** (they do not require a desktop). Then install the overlay so it can be started from the Weston session.
### 5.1 Daemon and API (systemd)
Use the same procedure as [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md). From the repo or your file server:
```bash
# Option A: deployment script (from device, with files present or via URL)
sudo ./deploy-screen-brightness-to-device.sh
# Or: sudo ./deploy-screen-brightness-to-device.sh "http://your-fileserver:5000/files"
# Option B: manual install (see SCREEN-BRIGHTNESS-MANUAL-SETUP.md)
sudo install -m 755 screen-brightness.py /usr/local/bin/
sudo install -m 644 screen-brightness.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now screen-brightness.service
```
Then install the **brightness API**:
```bash
sudo install -m 755 brightness-api.py /usr/local/bin/
sudo install -m 644 brightness-api.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now brightness-api.service
```
Confirm both are running:
```bash
systemctl status screen-brightness.service brightness-api.service
curl -s http://127.0.0.1:8765/state
```
### 5.2 Overlay (session app)
Install the overlay binary and its **Wayland layer-shell** dependency:
```bash
sudo apt-get install -y gir1.2-gtklayershell-0.1
sudo install -m 755 brightness-overlay.py /usr/local/bin/brightness-overlay.py
```
The overlay will be started by the Weston session script (below); no desktop autostart is needed.
---
## 6. Weston session: Chromium kiosk + overlay
Create a session that starts **Weston** with a custom script that launches **Chromium in kiosk mode** and the **brightness overlay**. When Chromium exits, you can either leave the session as-is or relaunch Chromium so the user cannot use anything else.
### 6.1 Kiosk URL and Chromium flags
Choose the URL your kiosk should open (e.g. your bridge or portal). Example:
- `https://your-portal.example.com/`
- Or a local page: `file:///usr/share/kiosk/index.html`
Chromium flags used here:
- `--kiosk` — Fullscreen, no browser chrome.
- `--noerrdialogs` — No error dialogs.
- `--disable-infobars` — No "Chrome is being controlled" bar.
- `--no-first-run` — Skip first-run experience.
- `--disable-session-crashed-bubble` — No "Restore pages?".
- `--start-fullscreen`
- Optional: `--autoplay-policy=no-user-gesture-required` if you need autoplay.
### 6.2 Weston startup script
Create a script that Weston will run as the session. It starts the overlay and Chromium; optionally restarts Chromium when it exits.
```bash
sudo install -d /usr/local/share/weston
sudo nano /usr/local/share/weston/kiosk-session.sh
```
Contents (replace `KIOSK_URL` with your URL):
```bash
#!/bin/bash
# Weston session: brightness overlay + Chromium kiosk.
# Run in Weston's compositor process so WAYLAND_DISPLAY is set.
KIOSK_URL="${KIOSK_URL:-https://example.com/}"
# Start overlay (uses GtkLayerShell; stays on top)
/usr/local/bin/brightness-overlay.py &
OVERLAY_PID=$!
# Optional: restart Chromium when it exits (user cannot escape to desktop)
while true; do
chromium-browser \
--kiosk \
--noerrdialogs \
--disable-infobars \
--no-first-run \
--disable-session-crashed-bubble \
--disable-restore-session-state \
--start-fullscreen \
"$KIOSK_URL"
sleep 2
done
# If not using the loop, just run chromium-browser once (user sees Weston when it closes).
```
Make it executable:
```bash
sudo chmod +x /usr/local/share/weston/kiosk-session.sh
```
Set the URL (e.g. via environment or by editing the script):
```bash
# Example: set default URL
echo 'KIOSK_URL="https://your-portal.example.com/"' | sudo tee /etc/default/weston-kiosk
# Then in kiosk-session.sh, source it: . /etc/default/weston-kiosk 2>/dev/null || true
```
### 6.3 Launch Weston with the session script
You need Weston to start with your script instead of the default shell. Options:
**Option A — Weston and shell (common):**
Start Weston so it runs a shell that runs your script:
```bash
weston -- -- /usr/local/share/weston/kiosk-session.sh
```
**Option B — systemd user or getty replacement:**
Create a systemd service that runs on the console (e.g. `tty1`) and executes:
```bash
/usr/bin/weston --tty=1 -- -- /usr/local/share/weston/kiosk-session.sh
```
That way the kiosk starts automatically at boot (see [Autologin and starting Weston at boot](#7-autologin-and-starting-weston-at-boot)).
---
## 7. Autologin and starting Weston at boot
To have the device boot straight into the kiosk (no login):
1. **Autologin** — Configure getty or your login manager so the kiosk user is logged in on `tty1` (e.g. Raspberry Pi OS "Auto login" or `getty@tty1` override with `ExecStart=-/sbin/agetty --autologin pi --noclear %I $TERM`).
2. **Run Weston from the autologin shell** — In the kiosk user's `.bash_profile` or `.profile`, start Weston only when on the console (so you don't start a second session over SSH):
```bash
# In ~/.profile or ~/.bash_profile (for user e.g. pi)
if [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
exec /usr/bin/weston --tty=1 -- -- /usr/local/share/weston/kiosk-session.sh
fi
```
Then reboot; the kiosk should start after autologin.
Alternatively, use a **systemd service** that runs on boot and starts Weston on `tty1` (replacing the getty for `tty1`). That avoids depending on shell profile and gives a single place to manage the kiosk.
---
## 8. Weston background (optional)
Weston can show a background image or colour. It is visible when Chromium is not fullscreen (e.g. at session start, or if Chromium exits and you are not auto-restarting it).
Configure in `weston.ini` (e.g. `~/.config/weston.ini` or `/etc/xdg/weston/weston.ini`). In the `[shell]` section:
- **Solid colour:** `background-color=0xff002244` (ARGB hex)
- **Image:** `background-image=/path/to/your/wallpaper.png`
---
## 9. Summary checklist
| Step | Action |
|------|--------|
| 1 | Use Raspberry Pi OS **Lite** (or convert to it). |
| 2 | Install reTerminal DM drivers (backlight, sensor, buzzer). |
| 3 | Install `weston`, `chromium-browser`, and (for overlay) `gir1.2-gtklayershell-0.1`. |
| 4 | Install **screen-brightness** and **brightness-api** as systemd services. |
| 5 | Install **brightness-overlay.py** and start it from the Weston session script. |
| 6 | Create **kiosk-session.sh** that starts overlay + Chromium (with optional Chromium restart loop). |
| 7 | Start Weston at boot (autologin + `.profile` or systemd) with `kiosk-session.sh`. |
After this, the device runs **Lite + Wayland** with no full GUI, only Chromium in kiosk mode and the brightness overlay on top. Your existing screen brightness control (daemon + API + overlay) continues to work; the overlay uses GtkLayerShell so it stays above Chromium.
---
## 10. Optional: hardening
- **Restrict TTYs** — Disable or limit getty on other TTYs so users cannot switch to a shell (e.g. mask `getty@tty2` … `getty@tty6` or restrict access).
- **Kiosk user** — Use a dedicated user (e.g. `kiosk`) with minimal privileges and no password or a random one; autologin that user on `tty1`.
- **Read-only root** — For maximum lock-down, consider a read-only root filesystem and overlayfs for temporary writes (document separately).
- **Network** — Restrict outbound traffic if the kiosk only needs to reach specific hosts.
---
## 11. References
- [Weston](https://wayland.freedesktop.org/docs/html/) — Wayland reference compositor.
- [Raspberry Pi OS](https://www.raspberrypi.com/software/) — Lite image.
- [Seeed reTerminal DM](https://wiki.seeedstudio.com/reterminal-dm/) — Hardware and drivers.
- [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) — Brightness daemon, API, and overlay installation and configuration.

View File

@@ -0,0 +1,276 @@
# Screen Brightness Service — Manual Setup on Existing Device
This guide describes how to install and configure the **screen-brightness** daemon on an existing reTerminal DM (e.g. a device that was not provisioned with the first-boot flow that includes step 14). The daemon controls LCD backlight from the ambient light sensor and raises brightness when the buzzer is active (alarm), with behaviour aligned to bridge display regulations.
**See also:** [SCREEN-BRIGHTNESS-REGULATIONS.md](SCREEN-BRIGHTNESS-REGULATIONS.md) for the regulatory references.
---
## 1. Prerequisites
- **Hardware:** reTerminal DM (Raspberry Pi CM4-based) with built-in light sensor and buzzer.
- **OS:** Raspberry Pi OS (or compatible) with:
- Python 3
- Sysfs nodes present (see [Verify hardware](#2-verify-hardware-below)).
No extra Python packages are required (stdlib only).
---
## 2. Verify hardware
On the device, confirm that the backlight, buzzer, and light sensor are available:
```bash
# Backlight (values 05)
cat /sys/class/backlight/lcd_backlight/brightness
cat /sys/class/backlight/lcd_backlight/max_brightness
# Buzzer (0 = off, 1 = on)
cat /sys/class/leds/usr-buzzer/brightness
# Ambient light (lux)
cat /sys/bus/iio/devices/iio:device0/in_illuminance_input
```
If any of these paths are missing, install the reTerminal DM drivers first (Seeed wiki: [reTerminal DM Getting Started](https://wiki.seeedstudio.com/reterminal-dm/)).
---
## 3. Install the service (manual copy)
### 3.1 Create directories and copy files
Run as **root** (or with `sudo`):
```bash
# Script
install -m 755 screen-brightness.py /usr/local/bin/screen-brightness.py
# Systemd unit
install -m 644 screen-brightness.service /etc/systemd/system/screen-brightness.service
# Optional: default config (skip if you already have vessel-specific /etc/screen-brightness.conf)
install -m 644 screen-brightness.conf /etc/screen-brightness.conf
```
Use the files from this repo:
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.py`
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.service`
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.conf`
Copy them to the device first (e.g. via SCP, USB stick, or the deployment script below), then run the `install` commands on the device.
### 3.2 Runtime directory and tmpfiles
Ensure the daemons runtime directory exists and is recreated after reboot:
```bash
mkdir -p /run/screen-brightness
# Create tmpfiles.d rule so the directory exists after reboot
cat > /etc/tmpfiles.d/screen-brightness.conf << 'EOF'
d /run/screen-brightness 0755 root root -
EOF
```
### 3.3 Enable and start
```bash
systemctl daemon-reload
systemctl enable screen-brightness.service
systemctl start screen-brightness.service
```
Check status and logs:
```bash
systemctl status screen-brightness.service
journalctl -u screen-brightness.service -f
```
---
## 4. Install using the deployment script
A helper script can install or update the service from **either** a local directory (repo clone/USB) **or** a URL (e.g. your file server). The script lives in the repo at:
`emmc-provisioning/scripts/deploy-screen-brightness-to-device.sh`
### 4.1 On the device (files already on the device)
If you have the three files in a directory on the reTerminal DM (e.g. copied from USB or repo):
```bash
cd /path/to/dir # directory containing screen-brightness.py, .service, .conf
sudo ./deploy-screen-brightness-to-device.sh
```
The script will install the files, create the tmpfiles.d rule, and start/enable the service. It will **not** overwrite an existing `/etc/screen-brightness.conf` (so vessel-specific tuning is preserved).
### 4.2 On the device (download from URL)
If your file server serves the three files (e.g. from the provisioning portal), pass the base URL as the **first argument** (recommended — works even when `sudo` does not preserve environment variables):
```bash
sudo ./deploy-screen-brightness-to-device.sh "http://file.server:5000/files"
```
Alternatively, if your `sudo` preserves the environment:
```bash
sudo -E BOOTSTRAP_URL="http://file.server:5000/files" ./deploy-screen-brightness-to-device.sh
```
The script will download `screen-brightness.py`, `screen-brightness.service`, and `screen-brightness.conf` from that base URL and show progress per file.
### 4.3 From a host (deploy via SSH)
From your laptop or build host, with the repo checked out and SSH access to the device:
```bash
cd /path/to/repo/emmc-provisioning/scripts
./deploy-screen-brightness-to-device.sh root@192.168.1.100
```
The script will copy the files from `../cloud-init/fileserver/` and run the install steps over SSH.
### 4.4 From a host via a jump server (bastion)
If the reTerminal DM is only reachable through a jump host, set `SSH_JUMP_HOST` and pass the device as the first argument:
```bash
cd /path/to/repo/emmc-provisioning/scripts
SSH_JUMP_HOST="user@jump.example.com" ./deploy-screen-brightness-to-device.sh pi@10.0.0.5
```
The script uses OpenSSHs `-J` / `ProxyJump` so `ssh` and `scp` go through the jump host to the device.
**Alternative:** put the jump in your SSH config so no env var is needed:
```
# ~/.ssh/config
Host reterminal-guard
HostName 10.0.0.5
User pi
ProxyJump user@jump.example.com
```
Then run:
```bash
./deploy-screen-brightness-to-device.sh reterminal-guard
```
---
## 5. Configuration
- **Config file:** `/etc/screen-brightness.conf` (INI format, optional).
If the file is missing, the daemon uses built-in defaults.
- **Reload config without restart:**
`sudo systemctl reload screen-brightness`
- **Override (navigator preset):**
- Force a fixed level (15):
`echo 3 | sudo tee /run/screen-brightness/override`
- Return to automatic (ambient + buzzer):
`echo auto | sudo tee /run/screen-brightness/override`
See comments in `screen-brightness.conf` for lux thresholds and level mapping (NIGHT / DUSK / DAY).
### 5.1 Crew brightness overlay (manual control)
To satisfy the requirement that the navigator can manually control brightness (MSC.191(79) §8.1.2), you can use the **brightness overlay**: a small panel that sits on top of the kiosk (Wayland layer-shell or X11) with buttons **1** **2** **3** **4** **5** and **Auto**.
- **brightness-api.service** — HTTP API on `127.0.0.1:8765`. Accepts `POST /brightness` with `level=1|2|3|4|5|auto`. Runs as root and writes `/run/screen-brightness/override`.
- **brightness-overlay.py** — Gtk3 overlay (same pattern as five-tap-close-chromium). Starts from session autostart; calls the API when a button is tapped.
If step 14 installed them, the overlay appears at the **bottom-right** after login. To install manually on an existing device:
```bash
# API (run as root)
sudo install -m 755 brightness-api.py /usr/local/bin/
sudo install -m 644 brightness-api.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now brightness-api.service
# Overlay (run as pi, autostart)
install -m 755 brightness-overlay.py ~/brightness-overlay.py
# Then add brightness-overlay.desktop to ~/.config/autostart/ (Exec path = your $HOME/brightness-overlay.py)
```
Dependencies: Python 3, PyGObject (Gtk3). On Wayland, `gir1.2-gtklayershell-0.1` for the overlay layer.
---
## 6. State and dashboard integration
The daemon writes a state file for other services (e.g. your alarm dashboard):
```bash
cat /run/screen-brightness/state
```
Example:
```
mode=ALERT
level=5
lux=12
buzzer=1
override=0
updated=1741266000
```
Your dashboard can read this to show current brightness mode or to drive a “night mode” indicator.
---
## 7. Testing buzzer and brightness
To confirm that the screen goes to full brightness when the buzzer is active and stays up during cooldown, run a short beep test on the device:
```bash
# From the device (if you have the script in place)
sudo ./test-buzzer-brightness.sh
```
If the script is not on the device, fetch and run it from your file server (replace with your portal URL):
```bash
curl -fsSL "http://10.130.60.141:5000/files/first-boot/test-buzzer-brightness.sh" -o /tmp/test-buzzer-brightness.sh
chmod +x /tmp/test-buzzer-brightness.sh
sudo /tmp/test-buzzer-brightness.sh
```
The script beeps four times. While it runs, the screen should go to maximum brightness (ALERT) and stay high for about 10 seconds after the last beep (COOLDOWN), then return to ambient level.
---
## 8. Troubleshooting
| Symptom | Check |
|--------|--------|
| Service fails to start | `journalctl -u screen-brightness.service -n 50`; confirm sysfs paths exist (step 2). |
| Brightness never changes | Verify light sensor: `cat /sys/bus/iio/devices/iio:device0/in_illuminance_input` (value should change when you shade the sensor). |
| Buzzer does not raise brightness | 1) Ensure service is running: `systemctl status screen-brightness.service`. 2) Use `test-buzzer-brightness.sh` (it uses the buzzers `max_brightness`, often 255). 3) Confirm buzzer sysfs updates while ON: `cat /sys/class/leds/usr-buzzer/brightness` (non-zero means ON), then `cat /run/screen-brightness/state` (mode=ALERT / COOLDOWN). |
| No logs from brightness service | Check that the unit is active: `systemctl is-active screen-brightness.service`. Then `journalctl -u screen-brightness.service -f` (follow). If the service exits, check `journalctl -u screen-brightness.service -n 50` for Python or path errors. |
| Brightness overlay not visible | 1) **Start it now:** from a terminal on the device (VNC or local), run: `python3 /home/pi/brightness-overlay.py` (or `~/brightness-overlay.py`). The panel should appear bottom-right; any errors will show in the terminal. 2) **After deploy:** autostart runs at login — log out and log back in so the overlay starts (it uses a 6 s delay). 3) **API required:** ensure `systemctl status brightness-api.service` is active; the overlay calls 127.0.0.1:8765. 4) **Dependencies:** need PyGObject (Gtk3); on Wayland, `gir1.2-gtklayershell-0.1` for the layer. |
| Config change not applied | Run `sudo systemctl reload screen-brightness` (or restart the service). |
---
## 9. File locations (reference)
| Item | Path |
|------|------|
| Daemon script | `/usr/local/bin/screen-brightness.py` |
| Systemd unit | `/etc/systemd/system/screen-brightness.service` |
| Config | `/etc/screen-brightness.conf` |
| Override (navigator) | `/run/screen-brightness/override` |
| State (read-only) | `/run/screen-brightness/state` |
| tmpfiles.d | `/etc/tmpfiles.d/screen-brightness.conf` |
| Brightness API (crew control) | `/usr/local/bin/brightness-api.py` |
| Brightness API unit | `/etc/systemd/system/brightness-api.service` |
| Overlay script | `$HOME/brightness-overlay.py` |
| Overlay autostart | `$HOME/.config/autostart/brightness-overlay.desktop` |

View File

@@ -0,0 +1,163 @@
# Screen Brightness — Maritime Regulations and References
This document summarises the international regulations and standards that apply to **display brightness and dimming** on ship bridges, and how the **screen-brightness** daemon on the reTerminal DM is aligned with them. It is intended for vessel operators, integrators, and auditors.
---
## 1. Overview
Bridge equipment that presents navigation-related or alarm information on a display is subject to:
- **IMO** (International Maritime Organization) resolutions, which set performance standards.
- **IEC** (International Electrotechnical Commission) and **IHO** (International Hydrographic Organization) standards, which give technical and test requirements.
The reTerminal DM screen-brightness daemon implements behaviour that satisfies the relevant clauses for:
- Adjustable brightness that is **never extinguished** (“not to extinction”).
- A **navigator-resettable** brightness/contrast preset (override).
- **High luminous intensity** for alarm/alert states.
- **Night mode** that does not degrade crew night vision.
- **Three operating modes** (NIGHT / DUSK / DAY) consistent with ECDIS-type display practice.
---
## 2. IMO MSC.191(79) — Navigational displays
**Full title:**
*Performance Standards for the Presentation of Navigation-Related Information on Shipborne Navigational Displays*
**Adoption:** 6 December 2004
**Applicability:** Displays that present navigation-related information on the bridge (including ECDIS and other navigational displays). Often referenced for equipment installed after 1 July 2008.
**Source (external):**
- [imorules.com MSC.191(79)](https://www.imorules.com/MSCRES_191.79.html)
- IMO official: MSC.191(79) in the IMO Publications catalogue.
### 2.1 §8.1 Display adjustment
| Requirement | Text (summary) | Implementation in screen-brightness |
|-------------|-----------------|-------------------------------------|
| Adjustability | Contrast and brightness shall be adjustable as applicable to the display technology. | Brightness is controlled in steps 15 from ambient sensor and/or override. |
| Dimming | The display shall be capable of being **dimmed**. | NIGHT and DUSK modes set low backlight levels (12). |
| Legibility | The range of control shall allow the display to be **legible under all ambient light conditions**. | Lux thresholds map to levels 15 so the display remains usable from night to bright day. |
| **Not to extinction** | Implicit in “dimmed” and “legible under all conditions”: the display must not be turned off. | **Level 0 is never used.** Minimum level is 1 (“not to extinction”). |
### 2.2 §8.1.2 Preset / default
| Requirement | Text (summary) | Implementation in screen-brightness |
|-------------|-----------------|-------------------------------------|
| Reset to preset | The navigator shall be able to **reset** contrast and/or brightness to a **preset or default condition**. | Override file: `echo <15> \| sudo tee /run/screen-brightness/override` sets a fixed level; `echo auto` restores automatic (ambient + buzzer) behaviour. |
**Reference (section 8.1):**
- [imorules.com 8.1 Display adjustment](https://www.imorules.com/GUID-6B14EB44-F913-4C8F-8E36-F74CE6E8E3DE.html)
### 2.3 Position of the brightness control
**MSC.191(79), MSC.302(87), IHO S-52, and IEC 62288 do not specify where the brightness (or contrast) control must be located on the screen or in the user interface.** They only require that:
- Brightness be adjustable and dimmable.
- The navigator be able to reset to a preset or default.
So the **position of the brightness control** (e.g. bottom-right overlay icon on the reTerminal DM) is an **implementation and design choice**, not a regulatory requirement. Placement can follow human-factors or vessel-specific guidelines (e.g. readily accessible, non-intrusive) as long as the above requirements are met.
---
## 3. IMO MSC.302(87) — Bridge alert management
**Full title:**
*Adoption of Performance Standards for Bridge Alert Management*
**Adoption:** 17 May 2010
**Applicability:** Bridge Alert Management (BAM) and presentation of alerts on the bridge. Recommended for central alert management (CAM) and CAM human-machine interface (CAM-HMI) installed on or after 1 July 2014.
**Source (external):**
- [imorules.com MSC.302(87)](https://www.imorules.com/MSCRES_302.87.html)
- IMO official: MSC.302(87) PDF in IMO Knowledge Centre.
### 3.1 Visual alarms and night vision
| Requirement | Text (summary) | Implementation in screen-brightness |
|-------------|-----------------|-------------------------------------|
| Night vision | Visual alarms and indicators shall **not interfere with night vision**. | In ambient mode, NIGHT/DUSK use low levels (12); transitions are smooth (one step per poll) to avoid sudden bright flashes. |
| Dimming not to extinction | **Dimming facilities** shall be incorporated, **though not to extinction**. | Same as MSC.191(79): level 0 is never used; minimum is level 1. |
| High luminous intensity for alerts | Supplemental **visual indicators** (e.g. for alarms) shall be of **high luminous intensity**. | When the **buzzer** is ON (alarm), backlight is set to **level 5 (maximum)** immediately, with no smooth stepping. |
| Persistence after alarm | Alerts must remain visible/audible long enough for crew response. | After the buzzer stops, brightness is held at level 5 for a **cooldown period** (default 10 s), then stepped down; duration is configurable in `/etc/screen-brightness.conf`. |
**Reference:**
- IMO MSC.302(87) Annex — Performance Standards for Bridge Alert Management (Modules AD).
- [imorules.com MSC.302(87) Annex](https://www.imorules.com/MSCRES_302.87_ANN.html)
---
## 4. IHO S-52 and IEC 62288 — ECDIS and display presentation
**IHO S-52** (International Hydrographic Organization) specifies chart content and **display aspects** for ECDIS, including colour and luminance behaviour. **IEC 62288** implements and tests these and related IMO requirements for shipborne navigational displays.
### 4.1 Three colour / luminance modes
| Mode | Typical use | Implementation in screen-brightness |
|------|-------------|-------------------------------------|
| **Day** | Bright ambient; high contrast. | DAY-DIM, DAY-NORMAL, DAY-BRIGHT (levels 35) by lux bands. |
| **Dusk/Dawn** | Transition; reduced luminance. | DUSK mode → level 2. |
| **Night** | Low luminance so as **not to affect the mariners night vision**. | NIGHT mode → level 1 (minimum). |
**IHO S-52** (e.g. Edition 6.1.1):
- “The ambient lighting on the bridge varies between the extremes of bright sunlight … and night, when the **light emitted by the display has to be low enough that it does not affect the mariner's night vision**.”
- Three colour/luminance modes (day, dusk/dawn, night) are defined for ECDIS; the screen-brightness daemon provides a compatible **three-mode luminance framework** (NIGHT / DUSK / DAY-*).
**IEC 62288** (e.g. IEC 62288:2021):
- General requirements, test methods, and required results for the **presentation of navigation-related information** on shipborne navigational displays.
- Supports IMO MSC.191(79) and MSC.302(87).
- Full luminance limits (e.g. numerical cd/m² for night) are in the purchased standard; the daemons “minimum level 1” and “never 0” satisfy the “dimmed but not to extinction” and “night vision” intent.
**Sources (external):**
- IHO S-52 — Specifications for chart content and display aspects of ECDIS (IHO publication).
- [IEC 62288:2021](https://webstore.iec.ch/publication/64659) — Maritime navigation and radiocommunication equipment and systems Presentation of navigation-related information on shipborne navigational displays.
---
## 5. Summary table — Regulation vs implementation
| Regulation | Clause / topic | Daemon behaviour |
|------------|----------------|------------------|
| **MSC.191(79)** | §8.1.1 Brightness adjustable, dimmable | Levels 15 from ambient sensor; configurable lux→level mapping. |
| **MSC.191(79)** | §8.1.1 Legible in all ambient conditions | NIGHT / DUSK / DAY-* modes with configurable lux thresholds. |
| **MSC.191(79)** | §8.1.1 Not to extinction | Level **0 never used**; minimum level 1. |
| **MSC.191(79)** | §8.1.2 Reset to preset | Override file: fixed level 15 or `auto`. |
| **MSC.302(87)** | Visual alarms high luminance | Buzzer ON → **immediate** level 5. |
| **MSC.302(87)** | Dimming not to extinction | Same as above; min level 1. |
| **MSC.302(87)** | Night vision not degraded | NIGHT/DUSK low levels; smooth stepping on transitions. |
| **MSC.302(87)** | Alert persistence | Cooldown (e.g. 10 s) at level 5 after buzzer stops. |
| **IHO S-52 / IEC 62288** | Three modes (day / dusk / night) | NIGHT, DUSK, DAY-DIM, DAY-NORMAL, DAY-BRIGHT. |
| **IHO S-52** | Night luminance low for night vision | NIGHT mode → level 1. |
---
## 6. Configuration and audit
- **Config file:** `/etc/screen-brightness.conf` — lux thresholds, levels per mode, cooldown, hysteresis.
- **Override (preset):** `/run/screen-brightness/override` — navigator preset (15 or `auto`).
- **State (read-only):** `/run/screen-brightness/state` — current mode, level, lux, buzzer, override.
- **Reload:** `systemctl reload screen-brightness` (no restart).
For vessel-specific tuning or audits, adjust thresholds and levels in `/etc/screen-brightness.conf` and document the chosen values and the fact that level 0 is never used.
---
## 7. References (URLs and documents)
| Reference | Description | URL or source |
|-----------|-------------|----------------|
| IMO MSC.191(79) | Performance standards for presentation of navigation-related information on shipborne navigational displays | https://www.imorules.com/MSCRES_191.79.html |
| IMO MSC.191(79) §8.1 | Display adjustment (brightness, dimming, preset) | https://www.imorules.com/GUID-6B14EB44-F913-4C8F-8E36-F74CE6E8E3DE.html |
| IMO MSC.302(87) | Performance standards for Bridge Alert Management | https://www.imorules.com/MSCRES_302.87.html |
| IMO MSC.302(87) Annex | BAM performance standards (Modules AD) | https://www.imorules.com/MSCRES_302.87_ANN.html |
| IHO S-52 | Specifications for chart content and display aspects of ECDIS (e.g. Ed. 6.1.1) | IHO publication; see https://iho.int |
| IEC 62288 | Presentation of navigation-related information on shipborne navigational displays | IEC 62288:2021 (and amendments); https://webstore.iec.ch |
| reTerminal DM wiki | Hardware interfaces (backlight, buzzer, light sensor) | https://wiki.seeedstudio.com/reterminal-dm/ |
---
*This document is for guidance only. For formal compliance, refer to the official IMO, IHO, and IEC publications and your flag state / classification society.*

View File

@@ -0,0 +1,62 @@
# Screen Brightness Control — Summary
Short overview of how the reTerminal DM brightness system works.
---
## What it does
---
## Components
| Component | Role |
|-----------|------|
| **screen-brightness** (daemon) | Reads sensor and override, drives backlight and buzzer; writes `/run/screen-brightness/state`. |
| **brightness-api** (HTTP) | Listens on `127.0.0.1:8765`. POST sets override (level 15 or auto); GET returns current state. |
| **brightness-overlay** (UI) | Small icon in bottom-right of screen; click to expand/collapse panel with buttons 15 and Auto. Talks to the API. |
---
## Levels and modes
- **Levels 15:** 1 = dimmest, 5 = brightest. Written to `/sys/class/backlight/lcd_backlight/brightness` (0 is never used).
- **Auto:** Override file set to `auto` (or missing). Daemon uses ambient light to pick level.
- **Manual:** Override file contains `1``5`. Daemon keeps that level until user switches back to Auto.
Override file: `/run/screen-brightness/override`
State file (read-only): `/run/screen-brightness/state` (mode, level, lux, override=0|1).
---
## On-screen overlay
- **Icon:** Brightness (sun) icon in the bottom-right corner; transparent when retracted.
- **Click icon:** Panel expands with “Brightness”, buttons **1 2 3 4 5**, and **Auto**. Semi-transparent background behind the buttons.
- **Active state:** The current level or Auto is highlighted so you can see which is active.
- **Click again:** Panel retracts to icon only.
Overlay uses the local API; if GtkLayerShell is available (Wayland), it stays on top of fullscreen apps.
---
## Services and boot
- **screen-brightness.service** — Starts the daemon. `ExecStartPre` writes `auto` into the override file so every boot starts in Auto.
- **brightness-api.service** — Starts the HTTP API (after `screen-brightness`).
- **brightness-overlay** — Started from the user session (e.g. autostart) so it appears on the desktop.
---
## Quick reference
| Action | How |
|--------|-----|
| Set level 3 | Overlay: expand → click **3**. Or: `echo 3 > /run/screen-brightness/override` (root). |
| Set Auto | Overlay: expand → click **Auto**. Or: `echo auto > /run/screen-brightness/override`. |
| Read state | `curl http://127.0.0.1:8765/state` or `cat /run/screen-brightness/state`. |
| Config | `/etc/screen-brightness.conf` (optional); `systemctl reload screen-brightness` to apply. |
For full setup and regulations, see [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) and [SCREEN-BRIGHTNESS-REGULATIONS.md](SCREEN-BRIGHTNESS-REGULATIONS.md).

View File

@@ -4,7 +4,7 @@
# When using setup-network-boot-on-lxc.sh, the actual subnet, DHCP range, and # When using setup-network-boot-on-lxc.sh, the actual subnet, DHCP range, and
# file.server address come from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh). # file.server address come from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh).
# Listen only on eth1 (provisioning LAN) # Listen only on eth1 (provisioning LAN); setup script adds listen-address for primary + 192.168.30.1, 192.168.127.1, 192.168.0.1 (DNS on all)
interface=eth1 interface=eth1
bind-interfaces bind-interfaces

View File

@@ -1,24 +0,0 @@
[2026-02-18T09:57:49+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095749.log
[2026-02-18T09:57:49+02:00] Deploying to root@10.130.60.224 ...
[2026-02-18T09:57:49+02:00] [1/4] Cleaning remote staging dir ...
[2026-02-18T09:57:50+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
[2026-02-18T09:57:50+02:00] [3/4] Running remote install (host + LXC) ...
[2026-02-18T08:01:21+00:00] LXC 201 already exists.
[2026-02-18T08:01:21+00:00] Host: installing scripts and udev ...
[2026-02-18T08:01:22+00:00] Host: env and dirs ...
[2026-02-18T08:01:22+00:00] Starting LXC 201 if stopped ...
[2026-02-18T08:01:25+00:00] LXC: installing flash scripts ...
failed to create file: /opt/cm4-provisioning/: Is a directory
[2026-02-18T08:01:45+00:00] LXC: installing dashboard ...
failed to create file: /opt/cm4-provisioning/dashboard/: Is a directory
failed to create file: /opt/cm4-provisioning/dashboard/templates/: Is a directory
[2026-02-18T08:01:59+00:00] Deploy done (remote).
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
failed to create file: /opt/cm4-provisioning/dashboard/: Is a directory
[2026-02-18T09:58:31+02:00] [4/4] Deploy finished.
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095749.log

View File

@@ -1,20 +0,0 @@
[2026-02-18T09:58:59+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095859.log
[2026-02-18T09:58:59+02:00] Deploying to root@10.130.60.224 ...
[2026-02-18T09:58:59+02:00] [1/4] Cleaning remote staging dir ...
[2026-02-18T09:59:00+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
[2026-02-18T09:59:00+02:00] [3/4] Running remote install (host + LXC) ...
[2026-02-18T08:02:32+00:00] LXC 201 already exists.
[2026-02-18T08:02:32+00:00] Host: installing scripts and udev ...
[2026-02-18T08:02:32+00:00] Host: env and dirs ...
[2026-02-18T08:02:32+00:00] Starting LXC 201 if stopped ...
[2026-02-18T08:02:35+00:00] LXC: installing flash scripts ...
[2026-02-18T08:02:55+00:00] LXC: installing dashboard ...
[2026-02-18T08:03:09+00:00] Deploy done (remote).
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
[2026-02-18T09:59:41+02:00] [4/4] Deploy finished.
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095859.log

View File

@@ -1,20 +0,0 @@
[2026-02-18T10:11:19+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-101119.log
[2026-02-18T10:11:19+02:00] Deploying to root@10.130.60.224 ...
[2026-02-18T10:11:19+02:00] [1/4] Cleaning remote staging dir ...
[2026-02-18T10:11:20+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
[2026-02-18T10:11:21+02:00] [3/4] Running remote install (host + LXC) ...
[2026-02-18T08:14:52+00:00] LXC 201 already exists.
[2026-02-18T08:14:52+00:00] Host: installing scripts and udev ...
[2026-02-18T08:14:52+00:00] Host: env and dirs ...
[2026-02-18T08:14:52+00:00] Starting LXC 201 if stopped ...
[2026-02-18T08:14:56+00:00] LXC: installing flash scripts ...
[2026-02-18T08:15:16+00:00] LXC: installing dashboard ...
[2026-02-18T08:15:30+00:00] Deploy done (remote).
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
[2026-02-18T10:12:02+02:00] [4/4] Deploy finished.
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-101119.log

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env bash
# Deploy screen-brightness daemon to an existing reTerminal DM.
#
# Usage:
# On device (local files in current directory):
# sudo ./deploy-screen-brightness-to-device.sh
#
# On device (download from file server) — use ONE of these (sudo often strips env):
# sudo ./deploy-screen-brightness-to-device.sh "http://file.server:5000/files"
# sudo -E BOOTSTRAP_URL="http://file.server:5000/files" ./deploy-screen-brightness-to-device.sh
#
# From host (deploy via SSH; repo files used from ../cloud-init/fileserver/):
# ./deploy-screen-brightness-to-device.sh [user@]hostname
#
# From host via jump server (bastion):
# SSH_JUMP_HOST="user@jump.example.com" ./deploy-screen-brightness-to-device.sh pi@10.0.0.5
# Or in ~/.ssh/config set ProxyJump for the device; then no env var needed.
#
# Does not overwrite existing /etc/screen-brightness.conf (keeps vessel tuning).
# See docs/SCREEN-BRIGHTNESS-MANUAL-SETUP.md for full manual steps.
set -e
# Unconditional first output so we know the script is running (helps if run via sudo)
echo "screen-brightness deploy: starting..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" && pwd)"
FILESERVER_DIR="$(cd "$SCRIPT_DIR/../cloud-init/fileserver" 2>/dev/null && pwd)"
ARG1="${1:-}"
# If first argument looks like a URL (not user@host), use it as BOOTSTRAP_URL
if [[ -n "$ARG1" && "$ARG1" == http* && "$ARG1" != *"@"* ]]; then
BOOTSTRAP_URL="${ARG1%/}" # strip trailing slash for clean ${URL}/${name}
REMOTE=""
else
REMOTE="$ARG1"
fi
# Require root for local install
if [[ -z "$REMOTE" && "$(id -u)" -ne 0 ]]; then
echo "This script must be run as root for local install. Use: sudo $0 ${1:-}" >&2
exit 1
fi
# ── Remote deploy (from host via SSH) ─────────────────────────────────────────
# REMOTE is set when first arg is not a URL (e.g. pi@device or Host alias from ~/.ssh/config)
if [[ -n "$REMOTE" ]]; then
if [[ ! -d "$FILESERVER_DIR" ]]; then
echo "ERROR: Fileserver dir not found: $FILESERVER_DIR" >&2
exit 1
fi
# Optional: deploy via jump server (e.g. SSH_JUMP_HOST="user@jump.example.com")
SSH_OPTS=()
SCP_OPTS=()
if [[ -n "${SSH_JUMP_HOST:-}" ]]; then
SSH_OPTS=(-J "$SSH_JUMP_HOST")
SCP_OPTS=(-o "ProxyJump=$SSH_JUMP_HOST")
echo "Using jump host: $SSH_JUMP_HOST"
fi
echo "Deploying screen-brightness + overlay to $REMOTE (from $FILESERVER_DIR) ..."
ssh "${SSH_OPTS[@]}" "${REMOTE}" "mkdir -p /tmp/screen-brightness-deploy"
scp -q "${SCP_OPTS[@]}" \
"$FILESERVER_DIR/screen-brightness.py" \
"$FILESERVER_DIR/screen-brightness.service" \
"$FILESERVER_DIR/screen-brightness.conf" \
"$FILESERVER_DIR/brightness-api.py" \
"$FILESERVER_DIR/brightness-api.service" \
"$FILESERVER_DIR/brightness-overlay.py" \
"$FILESERVER_DIR/brightness-overlay-launch.sh" \
"$FILESERVER_DIR/brightness-overlay.desktop" \
"${REMOTE}:/tmp/screen-brightness-deploy/"
ssh "${SSH_OPTS[@]}" "${REMOTE}" "sudo bash -s" << 'REMOTE_SCRIPT'
set -e
cd /tmp/screen-brightness-deploy
install -m 755 screen-brightness.py /usr/local/bin/screen-brightness.py
install -m 644 screen-brightness.service /etc/systemd/system/screen-brightness.service
[[ ! -f /etc/screen-brightness.conf ]] && install -m 644 screen-brightness.conf /etc/screen-brightness.conf || true
mkdir -p /run/screen-brightness
printf '%s\n' 'd /run/screen-brightness 0755 root root -' > /etc/tmpfiles.d/screen-brightness.conf
systemctl daemon-reload
systemctl enable screen-brightness.service
systemctl restart screen-brightness.service
echo "Installed and restarted screen-brightness.service"
# Brightness API (for overlay)
install -m 755 brightness-api.py /usr/local/bin/brightness-api.py
install -m 644 brightness-api.service /etc/systemd/system/brightness-api.service
systemctl daemon-reload
systemctl enable brightness-api.service
systemctl restart brightness-api.service
echo "Installed and restarted brightness-api.service"
# Overlay (crew manual control) — install for user pi
OVERLAY_USER=pi
OVERLAY_HOME=/home/$OVERLAY_USER
if [[ -f brightness-overlay.py && -f brightness-overlay.desktop ]] && id "$OVERLAY_USER" &>/dev/null; then
install -m 755 brightness-overlay.py "$OVERLAY_HOME/brightness-overlay.py"
[[ -f brightness-overlay-launch.sh ]] && install -m 755 brightness-overlay-launch.sh "$OVERLAY_HOME/brightness-overlay-launch.sh"
mkdir -p "$OVERLAY_HOME/.config/autostart"
sed "s|/home/pi|$OVERLAY_HOME|g" brightness-overlay.desktop > "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
chown -R "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay.py" "$OVERLAY_HOME/brightness-overlay-launch.sh" "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop" 2>/dev/null || true
echo "Installed brightness overlay for $OVERLAY_USER (autostart; starts ~6s after login)"
else
echo "Skipped overlay (files missing or user $OVERLAY_USER not found)"
fi
REMOTE_SCRIPT
ssh "${SSH_OPTS[@]}" "${REMOTE}" "rm -rf /tmp/screen-brightness-deploy"
echo "Done. Check: ssh ${SSH_OPTS[*]} $REMOTE systemctl status screen-brightness.service"
exit 0
fi
# ── Local deploy (on device) ─────────────────────────────────────────────────
if [[ -n "${BOOTSTRAP_URL:-}" ]]; then
echo "Downloading from $BOOTSTRAP_URL ..."
DOWNLOAD_DIR="/tmp/screen-brightness-deploy"
mkdir -p "$DOWNLOAD_DIR"
REQUIRED="screen-brightness.py screen-brightness.service screen-brightness.conf"
OPTIONAL="brightness-api.py brightness-api.service brightness-overlay.py brightness-overlay-launch.sh brightness-overlay.desktop"
for name in $REQUIRED; do
url="${BOOTSTRAP_URL}/${name}"
if ! curl -fSL "$url" -o "$DOWNLOAD_DIR/$name" 2>/tmp/screen-brightness-curl-err; then
echo "ERROR: Failed to download $url" >&2
[[ -s /tmp/screen-brightness-curl-err ]] && cat /tmp/screen-brightness-curl-err >&2
rm -f /tmp/screen-brightness-curl-err
rm -rf "$DOWNLOAD_DIR"
exit 1
fi
rm -f /tmp/screen-brightness-curl-err
echo " $name OK"
done
for name in $OPTIONAL; do
url="${BOOTSTRAP_URL}/${name}"
if curl -fSL "$url" -o "$DOWNLOAD_DIR/$name" 2>/dev/null; then
echo " $name OK"
fi
done
DEPLOY_DIR="$DOWNLOAD_DIR"
else
if [[ -f "$SCRIPT_DIR/screen-brightness.py" ]]; then
DEPLOY_DIR="$SCRIPT_DIR"
elif [[ -d "$FILESERVER_DIR" && -f "$FILESERVER_DIR/screen-brightness.py" ]]; then
DEPLOY_DIR="$FILESERVER_DIR"
else
# Assume files are in current directory
DEPLOY_DIR="$(pwd)"
fi
if [[ ! -f "$DEPLOY_DIR/screen-brightness.py" ]]; then
echo "ERROR: screen-brightness.py not found." >&2
echo " Run from a directory containing screen-brightness.py, .service, .conf" >&2
echo " Or set BOOTSTRAP_URL to download from a file server." >&2
exit 1
fi
echo "Using files from: $DEPLOY_DIR"
fi
install -m 755 "$DEPLOY_DIR/screen-brightness.py" /usr/local/bin/screen-brightness.py
install -m 644 "$DEPLOY_DIR/screen-brightness.service" /etc/systemd/system/screen-brightness.service
if [[ ! -f /etc/screen-brightness.conf ]]; then
install -m 644 "$DEPLOY_DIR/screen-brightness.conf" /etc/screen-brightness.conf
echo "Installed /etc/screen-brightness.conf"
else
echo "Keeping existing /etc/screen-brightness.conf"
fi
mkdir -p /run/screen-brightness
printf '%s\n' 'd /run/screen-brightness 0755 root root -' > /etc/tmpfiles.d/screen-brightness.conf
systemctl daemon-reload
systemctl enable screen-brightness.service
systemctl restart screen-brightness.service
echo "Installed and restarted screen-brightness.service"
# Brightness API (for overlay)
if [[ -f "${DEPLOY_DIR:-}/brightness-api.py" ]]; then
install -m 755 "$DEPLOY_DIR/brightness-api.py" /usr/local/bin/brightness-api.py
if [[ -f "$DEPLOY_DIR/brightness-api.service" ]]; then
install -m 644 "$DEPLOY_DIR/brightness-api.service" /etc/systemd/system/brightness-api.service
systemctl daemon-reload
systemctl enable brightness-api.service
systemctl restart brightness-api.service
echo "Installed and restarted brightness-api.service"
fi
fi
# Overlay (crew manual control)
OVERLAY_USER="${SUDO_USER:-pi}"
OVERLAY_HOME=$(getent passwd "$OVERLAY_USER" 2>/dev/null | cut -d: -f6)
OVERLAY_HOME="${OVERLAY_HOME:-/home/pi}"
if [[ -f "${DEPLOY_DIR:-}/brightness-overlay.py" && -f "${DEPLOY_DIR:-}/brightness-overlay.desktop" ]] && [[ -d "$OVERLAY_HOME" ]]; then
install -m 755 "$DEPLOY_DIR/brightness-overlay.py" "$OVERLAY_HOME/brightness-overlay.py"
[[ -f "${DEPLOY_DIR:-}/brightness-overlay-launch.sh" ]] && install -m 755 "$DEPLOY_DIR/brightness-overlay-launch.sh" "$OVERLAY_HOME/brightness-overlay-launch.sh"
mkdir -p "$OVERLAY_HOME/.config/autostart"
sed "s|/home/pi|$OVERLAY_HOME|g" "$DEPLOY_DIR/brightness-overlay.desktop" > "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
chown "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay.py" "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
[[ -f "$OVERLAY_HOME/brightness-overlay-launch.sh" ]] && chown "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay-launch.sh"
echo "Installed brightness overlay for $OVERLAY_USER (autostart; starts ~6s after login)"
fi
[[ "${DEPLOY_DIR:-}" == "/tmp/screen-brightness-deploy" ]] && rm -rf "$DEPLOY_DIR"
echo ""
systemctl status screen-brightness.service --no-pager || true

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Mount a Raspberry Pi OS .img or .img.xz, edit cloud-init NoCloud files on the boot # Mount a Raspberry Pi OS .img or .img.xz, edit cloud-init NoCloud files on the boot
# partition, then unmount and (if .img.xz) recompress. Requires sudo for loop/mount. # partition, then unmount and (if .img.xz) recompress to a NEW file with current
# date/time prefix. Original image is never overwritten. Requires sudo for loop/mount.
# #
# Usage: # Usage:
# ./edit-cloudinit-on-image.sh <path-to-image.img.xz> # ./edit-cloudinit-on-image.sh <path-to-image.img.xz>
@@ -13,7 +14,7 @@
# Example: # Example:
# ./edit-cloudinit-on-image.sh /path/to/gnss-bootstrap-20260223-215010.img.xz # ./edit-cloudinit-on-image.sh /path/to/gnss-bootstrap-20260223-215010.img.xz
# #
# Backup the image before running if you want to keep the original. # The original image is preserved; output uses a new timestamp (e.g. gnss-bootstrap-20250305-143022.img.xz).
set -e set -e
@@ -118,10 +119,19 @@ sudo losetup -d "$LOOP"
LOOP="" LOOP=""
if [[ -n "$ORIGINAL_XZ" && -z "$NO_RECOMPRESS" ]]; then if [[ -n "$ORIGINAL_XZ" && -z "$NO_RECOMPRESS" ]]; then
echo "Recompressing to $(basename "$ORIGINAL_XZ")" ORIG_DIR="$(dirname "$ORIGINAL_XZ")"
ORIG_BASE="$(basename "$ORIGINAL_XZ" .img.xz)"
if [[ "$ORIG_BASE" =~ ^(.+)-[0-9]{8}-[0-9]{6}$ ]]; then
BASE_NAME="${BASH_REMATCH[1]}"
else
BASE_NAME="$ORIG_BASE"
fi
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
NEW_IMG_XZ="$ORIG_DIR/${BASE_NAME}-${TIMESTAMP}.img.xz"
echo "Recompressing to $(basename "$NEW_IMG_XZ")"
xz -T 0 -z -f -k "$IMG_FILE" xz -T 0 -z -f -k "$IMG_FILE"
mv -f "${IMG_FILE}.xz" "$ORIGINAL_XZ" mv -f "${IMG_FILE}.xz" "$NEW_IMG_XZ"
echo "Done. Updated: $ORIGINAL_XZ" echo "Done. New image (original unchanged): $NEW_IMG_XZ"
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
WORK_DIR="" WORK_DIR=""
elif [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then elif [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then

View File

@@ -79,13 +79,12 @@ if ! command -v vconfig >/dev/null 2>&1; then
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq vlan apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq vlan
fi fi
# 2) dnsmasq config for eth1 only (DHCP + DNS). PXE/TFTP in network-boot-pxe.conf when needed (toggle-network-boot-dhcp.sh) # 2) dnsmasq config: DHCP on eth1 only; DNS on all interfaces (no bind-interfaces/listen-address).
# PXE/TFTP in network-boot-pxe.conf when needed (toggle-network-boot-dhcp.sh)
mkdir -p /etc/dnsmasq.d mkdir -p /etc/dnsmasq.d
cat > /etc/dnsmasq.d/network-boot.conf << DNSMASQ cat > /etc/dnsmasq.d/network-boot.conf << DNSMASQ
# DHCP + DNS on eth1 only (provisioning LAN) # DHCP on eth1 only; DNS on all interfaces (eth0, eth1, eth1.40, etc.)
# (TFTP/PXE options in network-boot-pxe.conf when network boot is enabled)
interface=eth1 interface=eth1
bind-interfaces
dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},12h dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},12h
# DNS: file.server resolves to this host (eth1) so scripts can use http://file.server/... # DNS: file.server resolves to this host (eth1) so scripts can use http://file.server/...
address=/file.server/${LAN_GW} address=/file.server/${LAN_GW}

View File

@@ -12,11 +12,15 @@
# ├── first-boot.conf ← cloud-init runcmd downloads this (required) # ├── first-boot.conf ← cloud-init runcmd downloads this (required)
# ├── first-boot.conf.example ← reference # ├── first-boot.conf.example ← reference
# ├── bootstrap.sh ← user-data.bootstrap downloads this (main path) # ├── bootstrap.sh ← user-data.bootstrap downloads this (main path)
# ── first-boot/ FILE_SERVER points here # ── screen-brightness.pymanual deploy: .../files/screen-brightness.py
# ├── screen-brightness.service
# ├── screen-brightness.conf
# └── first-boot/ ← FILE_SERVER points here (first-boot + step 14)
# ├── steps/ # ├── steps/
# │ ├── 01-hostname.sh … 13-reboot.sh # │ ├── 01-hostname.sh … 14-screen_brightness.sh
# ├── start-chromium.sh # ├── start-chromium.sh
# ├── splash.png # ├── splash.png
# ├── screen-brightness.py (and .service, .conf)
# └── ... # └── ...
# #
# Usage: ./sync-portal-files-to-lxc.sh [user@lxc_ip] # Usage: ./sync-portal-files-to-lxc.sh [user@lxc_ip]
@@ -34,6 +38,10 @@ REMOTE_FIRST_BOOT="${REMOTE_PORTAL}/first-boot"
# Files we sync to the portal root (outside first-boot/) # Files we sync to the portal root (outside first-boot/)
PORTAL_ROOT_FILES=(first-boot.sh first-boot.conf first-boot.conf.example bootstrap.sh) PORTAL_ROOT_FILES=(first-boot.sh first-boot.conf first-boot.conf.example bootstrap.sh)
# Files from fileserver/ that we also copy to portal root so /files/<name> works
# (e.g. manual deploy: sudo ./deploy-screen-brightness-to-device.sh "http://HOST:5000/files")
FILESERVER_FILES_AT_PORTAL_ROOT=(screen-brightness.py screen-brightness.service screen-brightness.conf)
# ── Validate local files ──────────────────────────────────────────────── # ── Validate local files ────────────────────────────────────────────────
if [[ ! -d "$FILESERVER_DIR" ]]; then if [[ ! -d "$FILESERVER_DIR" ]]; then
echo "Error: fileserver dir not found: $FILESERVER_DIR" echo "Error: fileserver dir not found: $FILESERVER_DIR"
@@ -67,6 +75,13 @@ for f in "${PORTAL_ROOT_FILES[@]}"; do
echo "$f (not found locally, will skip)" echo "$f (not found locally, will skip)"
fi fi
done done
for f in "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}"; do
if [[ -f "$FILESERVER_DIR/$f" ]]; then
echo "$f (from fileserver/)"
else
echo "$f (not in fileserver/, will skip portal root copy)"
fi
done
echo "" echo ""
echo "first-boot/ assets ($REMOTE_FIRST_BOOT/):" echo "first-boot/ assets ($REMOTE_FIRST_BOOT/):"
CHANGES=$(rsync -avzn --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/" 2>&1 | grep -v '^\(sending\|sent\|total\|$\|building\|\.d\.\.\.\.\.\.\.\.\.\)' | head -40) CHANGES=$(rsync -avzn --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/" 2>&1 | grep -v '^\(sending\|sent\|total\|$\|building\|\.d\.\.\.\.\.\.\.\.\.\)' | head -40)
@@ -98,12 +113,23 @@ echo ""
echo "── Syncing fileserver/ → first-boot/ ──────────────────────────" echo "── Syncing fileserver/ → first-boot/ ──────────────────────────"
rsync -avz --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/" rsync -avz --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/"
# ── Copy screen-brightness (and similar) from fileserver to portal root ───
# So http://HOST:5000/files/screen-brightness.py works for manual deploy
echo ""
echo "── Copying fileserver files to portal root ───────────────────"
for f in "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}"; do
if [[ -f "$FILESERVER_DIR/$f" ]]; then
rsync -avz "$FILESERVER_DIR/$f" "$LXC:$REMOTE_PORTAL/"
echo "$f"
fi
done
# ── Check for extra files in portal root ──────────────────────────────── # ── Check for extra files in portal root ────────────────────────────────
echo "" echo ""
echo "── Checking for extra files in portal root ────────────────────" echo "── Checking for extra files in portal root ────────────────────"
REMOTE_FILES=$(ssh "$LXC" "find $REMOTE_PORTAL -maxdepth 1 -not -path $REMOTE_PORTAL -printf '%f\n' 2>/dev/null" | sort) REMOTE_FILES=$(ssh "$LXC" "find $REMOTE_PORTAL -maxdepth 1 -not -path $REMOTE_PORTAL -printf '%f\n' 2>/dev/null" | sort)
EXPECTED_FILES=$(printf '%s\n' "${PORTAL_ROOT_FILES[@]}" "first-boot" | sort -u) EXPECTED_FILES=$(printf '%s\n' "${PORTAL_ROOT_FILES[@]}" "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}" "first-boot" | sort -u)
EXTRA_FILES=$(comm -23 <(echo "$REMOTE_FILES") <(echo "$EXPECTED_FILES")) EXTRA_FILES=$(comm -23 <(echo "$REMOTE_FILES") <(echo "$EXPECTED_FILES"))
if [[ -z "$EXTRA_FILES" ]]; then if [[ -z "$EXTRA_FILES" ]]; then

View File