1 Commits
Author SHA1 Message Date
NeilClack e22b466bb3 Update RadiantWave - Fixed scaling issues 2026-08-23 18:03:14 -05:00
29 changed files with 521 additions and 915 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Build Dev ISO
on:
workflow_dispatch:
push:
branches:
- dev
env:
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave-dev
extra-trusted-public-keys = radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0=
jobs:
build-dev-iso:
runs-on: build-server
steps:
- name: Clone Repository
run: git clone --depth 1 --no-single-branch ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
- name: Checkout dev branch
run: git checkout dev
- name: Prepare output directory
run: mkdir -p /home/user/radiantwave
# --- Kiosk ISO ---
- name: Build kiosk ISO
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure .#nixosConfigurations.dev-iso.config.system.build.isoImage
- name: Rename kiosk ISO
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-kiosk-dev-iso-${ISO_DATE}.iso"
cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}"
echo "KIOSK_ISO_FILENAME=${FILENAME}" >> "$GITHUB_ENV"
- name: Move old dev images to archive
run: tools/archive-images.sh nclack@157.245.124.103 /mnt/infrastructure_data/files/images /mnt/infrastructure_data/files/images/archives 'radiantwave-kiosk-dev*'
- name: Copy kiosk ISO to fileserver
run: |
scp "/home/user/radiantwave/${KIOSK_ISO_FILENAME}" \
radiantwave-infra:/mnt/infrastructure_data/files/images/
- name: Cleanup kiosk ISO
run: rm -f "/home/user/radiantwave/${KIOSK_ISO_FILENAME}"
+43
View File
@@ -0,0 +1,43 @@
on:
workflow_dispatch:
env:
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA=
jobs:
build-android:
runs-on: build-server
steps:
- name: Checkout
run: git clone --depth 1 ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
- name: Prepare output directory
run: mkdir -p /home/user/radiantwave
- name: Build android kiosk ISO
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure .#nixosConfigurations.android-iso.config.system.build.isoImage
- name: Rename android kiosk ISO
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-android-iso-${ISO_DATE}.iso"
cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}"
echo "ANDROID_ISO_FILENAME=${FILENAME}" >> "$GITHUB_ENV"
# - name: Move old android images to archive
# run: |
# ssh nclack@157.245.124.103 'mv /mnt/infrastructure_data/files/images/radiantwave-android-iso* /mnt/infrastructure_data/files/images/archives/'
- name: Copy android kiosk ISO to fileserver
run: |
scp "/home/user/radiantwave/${ANDROID_ISO_FILENAME}" \
radiantwave-infra:/mnt/infrastructure_data/files/images/
- name: Cleanup android kiosk ISO
run: rm -f "/home/user/radiantwave/${ANDROID_ISO_FILENAME}"
@@ -0,0 +1,43 @@
on:
workflow_dispatch:
env:
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA=
jobs:
build-commercial:
runs-on: build-server
steps:
- name: Checkout
run: git clone --depth 1 ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
- name: Prepare output directory
run: mkdir -p /home/user/radiantwave
- name: Build commercial kiosk ISO
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure .#nixosConfigurations.commercial-iso.config.system.build.isoImage
- name: Rename commercial kiosk ISO
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-commercial-iso-${ISO_DATE}.iso"
cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}"
echo "COMMERCIAL_ISO_FILENAME=${FILENAME}" >> "$GITHUB_ENV"
# - name: Move old commercial images to archive
# run: |
# ssh nclack@157.245.124.103 'mv /mnt/infrastructure_data/files/images/radiantwave-commercial-iso* /mnt/infrastructure_data/files/images/archives/'
- name: Copy commercial kiosk ISO to fileserver
run: |
scp "/home/user/radiantwave/${COMMERCIAL_ISO_FILENAME}" \
radiantwave-infra:/mnt/infrastructure_data/files/images/
- name: Cleanup commercial kiosk ISO
run: rm -f "/home/user/radiantwave/${COMMERCIAL_ISO_FILENAME}"
+43
View File
@@ -0,0 +1,43 @@
on:
workflow_dispatch:
env:
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA=
jobs:
build-kiosk:
runs-on: build-server
steps:
- name: Checkout
run: git clone --depth 1 ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
- name: Prepare output directory
run: mkdir -p /home/user/radiantwave
- name: Build kiosk ISO
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure .#nixosConfigurations.kiosk-iso.config.system.build.isoImage
- name: Rename kiosk ISO
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-home-${ISO_DATE}.iso"
cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}"
echo "KIOSK_ISO_FILENAME=${FILENAME}" >> "$GITHUB_ENV"
# - name: Move old kiosk images to archive
# run: |
# ssh nclack@157.245.124.103 'mv /mnt/infrastructure_data/files/images/radiantwave-home* /mnt/infrastructure_data/files/images/archives/'
- name: Copy kiosk ISO to fileserver
run: |
scp "/home/user/radiantwave/${KIOSK_ISO_FILENAME}" \
radiantwave-infra:/mnt/infrastructure_data/files/images/
- name: Cleanup kiosk ISO
run: rm -f "/home/user/radiantwave/${KIOSK_ISO_FILENAME}"
+42
View File
@@ -0,0 +1,42 @@
on:
workflow_dispatch:
env:
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave https://nixos-raspberrypi.cachix.org
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=
jobs:
build-rpi:
runs-on: build-server
steps:
- name: Checkout
run: git clone --depth 1 ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
- name: Prepare output directory
run: mkdir -p /home/user/radiantwave
- name: Build Raspberry Pi image
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage
- name: Rename Raspberry Pi image
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-rpi-${ISO_DATE}.img.zst"
cp -L result/sd-image/*.img.zst "/home/user/radiantwave/${FILENAME}"
echo "RPI_IMAGE_FILENAME=${FILENAME}" >> "$GITHUB_ENV"
- name: Move old Raspberry Pi images to archive
run: tools/archive-images.sh nclack@157.245.124.103 /mnt/infrastructure_data/files/images /mnt/infrastructure_data/files/images/archives 'radiantwave-rpi*'
- name: Copy Raspberry Pi image to fileserver
run: |
scp "/home/user/radiantwave/${RPI_IMAGE_FILENAME}" \
radiantwave-infra:/mnt/infrastructure_data/files/images/
- name: Cleanup Raspberry Pi image
run: rm -f "/home/user/radiantwave/${RPI_IMAGE_FILENAME}"
-116
View File
@@ -1,116 +0,0 @@
# One build workflow per hardware family, identical on every branch — do not
# add per-branch copies. build-x86.yaml is the same file with a different matrix.
#
# The branch/ref selects the channel, nothing else does: the flake's `channel`
# block picks the channel manifest whose closure is baked as the app, the Attic
# repo, and the upgrade ref, so the same config names build dev images on `dev`
# and prod images on `main`.
#
# Trigger model:
# * push to `dev` -> build every development image
# * new git tag -> build every production image (tags live on main)
# * manual dispatch -> build the channel of the selected branch
#
# This file builds the Raspberry Pi variant:
# Raspberry Pi -> .config.system.build.sdImage
# `orion-lxc` emulates aarch64-linux, so this runs on the same runner.
name: Build Kiosk Image (Raspberry Pi)
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
env:
# Both channel caches plus the Raspberry Pi cachix, so a build can substitute
# from whichever channel the checked-out ref selects. tarball-ttl=0 keeps the
# eval-time manifest fetch (builtins.fetchurl in flake.nix) from reusing a
# stale copy cached by an earlier build on this persistent runner.
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev https://nixos-raspberrypi.cachix.org
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=
tarball-ttl = 0
jobs:
build:
name: ${{ matrix.label }}
runs-on: orion-lxc
strategy:
fail-fast: false
matrix:
include:
- label: rpi
system: radiantwavekiosk-rpi
target: sdImage
outdir: sd-image
ext: img.zst
arch: aarch64-linux
manifest: aarch64-manifest.json
steps:
- name: Clone repository
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
ref="${GITHUB_REF#refs/heads/}"
ref="${ref#refs/tags/}"
git clone --depth 1 --branch "$ref" \
ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
git log --oneline -1
- name: Determine channel
run: |
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
CHANNEL=$(nix eval --impure --raw .#channelName)
if [ "$CHANNEL" = "radiantwave" ]; then LABEL=prod; else LABEL=dev; fi
echo "CHANNEL=$CHANNEL" >> "$GITHUB_ENV"
echo "CHANNEL_LABEL=$LABEL" >> "$GITHUB_ENV"
echo "Building the '$CHANNEL' channel ($LABEL)"
# Fail fast if the closure the channel manifest announces is not in the
# channel's Attic cache. We bake that exact storePath, so without it the
# image would try to compile RadiantWave from source (which never works:
# this repo has no app source). Runs for both channels; RadiantWave CI
# publishes the manifest only after the matching Attic push succeeds.
- name: Verify published app closure is in the channel cache
run: |
set -euo pipefail
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
ATTIC=$(nix eval --impure --raw .#channelAtticUrl)
MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}"
MANIFEST=$(curl -fsSL "$MANIFEST_URL")
PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath')
echo "channel : $CHANNEL"
echo "attic : $ATTIC"
echo "manifest: $MANIFEST_URL"
echo "closure : $PUB_PATH"
HASH=$(basename "$PUB_PATH" | cut -d- -f1)
if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then
echo "ERROR: $PUB_PATH is not in $ATTIC; the image would compile RadiantWave from source."
exit 1
fi
- name: Prepare output directory
run: mkdir -p /var/lib/gitea-runner/radiantwave
- name: Build ${{ matrix.label }}
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure \
".#nixosConfigurations.${{ matrix.system }}.config.system.build.${{ matrix.target }}"
- name: Publish ${{ matrix.label }}
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-${{ matrix.label }}-${CHANNEL_LABEL}-${ISO_DATE}.${{ matrix.ext }}"
cp -L result/${{ matrix.outdir }}/*.${{ matrix.ext }} \
"/var/lib/gitea-runner/radiantwave/${FILENAME}"
scp "/var/lib/gitea-runner/radiantwave/${FILENAME}" \
157.245.124.103:/mnt/infrastructure_data/files/images/
rm -f "/var/lib/gitea-runner/radiantwave/${FILENAME}"
-132
View File
@@ -1,132 +0,0 @@
# One build workflow per hardware family, identical on every branch — do not
# add per-branch copies. build-rpi.yaml is the same file with a different matrix.
#
# The branch/ref selects the channel, nothing else does: the flake's `channel`
# block picks the channel manifest whose closure is baked as the app, the Attic
# repo, and the upgrade ref, so the same config names build dev images on `dev`
# and prod images on `main`.
#
# Trigger model:
# * push to `dev` -> build every development image
# * new git tag -> build every production image (tags live on main)
# * manual dispatch -> build the channel of the selected branch
#
# This file builds the x86_64 ISO variants:
# ISO variants -> .config.system.build.isoImage (storeContents pulls in the closure)
name: Build Kiosk Images (x86)
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
env:
# Both channel caches plus the Raspberry Pi cachix, so a build can substitute
# from whichever channel the checked-out ref selects. tarball-ttl=0 keeps the
# eval-time manifest fetch (builtins.fetchurl in flake.nix) from reusing a
# stale copy cached by an earlier build on this persistent runner.
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev https://nixos-raspberrypi.cachix.org
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=
tarball-ttl = 0
jobs:
build:
name: ${{ matrix.label }}
runs-on: orion-lxc
strategy:
fail-fast: false
matrix:
include:
# NOTE: Gitea schedules matrix jobs in reverse definition order on
# the single orion-lxc runner, so the list is reversed on purpose:
# android -> commercial -> kiosk means kiosk builds FIRST.
- label: android
system: android-iso
target: isoImage
outdir: iso
ext: iso
arch: x86_64-linux
manifest: manifest.json
- label: commercial
system: commercial-iso
target: isoImage
outdir: iso
ext: iso
arch: x86_64-linux
manifest: manifest.json
- label: kiosk
system: kiosk-iso
target: isoImage
outdir: iso
ext: iso
arch: x86_64-linux
manifest: manifest.json
steps:
- name: Clone repository
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
ref="${GITHUB_REF#refs/heads/}"
ref="${ref#refs/tags/}"
git clone --depth 1 --branch "$ref" \
ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
git log --oneline -1
- name: Determine channel
run: |
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
CHANNEL=$(nix eval --impure --raw .#channelName)
if [ "$CHANNEL" = "radiantwave" ]; then LABEL=prod; else LABEL=dev; fi
echo "CHANNEL=$CHANNEL" >> "$GITHUB_ENV"
echo "CHANNEL_LABEL=$LABEL" >> "$GITHUB_ENV"
echo "Building the '$CHANNEL' channel ($LABEL)"
# Fail fast if the closure the channel manifest announces is not in the
# channel's Attic cache. We bake that exact storePath, so without it the
# image would try to compile RadiantWave from source (which never works:
# this repo has no app source). Runs for both channels; RadiantWave CI
# publishes the manifest only after the matching Attic push succeeds.
- name: Verify published app closure is in the channel cache
run: |
set -euo pipefail
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
ATTIC=$(nix eval --impure --raw .#channelAtticUrl)
MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}"
MANIFEST=$(curl -fsSL "$MANIFEST_URL")
PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath')
echo "channel : $CHANNEL"
echo "attic : $ATTIC"
echo "manifest: $MANIFEST_URL"
echo "closure : $PUB_PATH"
HASH=$(basename "$PUB_PATH" | cut -d- -f1)
if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then
echo "ERROR: $PUB_PATH is not in $ATTIC; the image would compile RadiantWave from source."
exit 1
fi
- name: Prepare output directory
run: mkdir -p /var/lib/gitea-runner/radiantwave
- name: Build ${{ matrix.label }}
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure \
".#nixosConfigurations.${{ matrix.system }}.config.system.build.${{ matrix.target }}"
- name: Publish ${{ matrix.label }}
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-${{ matrix.label }}-${CHANNEL_LABEL}-${ISO_DATE}.${{ matrix.ext }}"
cp -L result/${{ matrix.outdir }}/*.${{ matrix.ext }} \
"/var/lib/gitea-runner/radiantwave/${FILENAME}"
scp "/var/lib/gitea-runner/radiantwave/${FILENAME}" \
157.245.124.103:/mnt/infrastructure_data/files/images/
rm -f "/var/lib/gitea-runner/radiantwave/${FILENAME}"
-2
View File
@@ -4,5 +4,3 @@ build/out/
.direnv
wifi-psk.txt
*.qcow2
VERIFY-DEV.md
VERIFY-PROD.md
+28 -35
View File
@@ -2,56 +2,50 @@
## What this repo is
NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The app itself is not pinned or built here: each channel's **published manifest** (served from `files.radiantwavetech.com`, pushed by RadiantWave CI to Attic) is the single source of truth for the baked closure.
NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The actual app lives in the `radiantwave` flake inputs (from gitea.radiantwavetech.com).
Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig`
## Prod/dev split
Two branches — `main` (prod) and `dev`. There are **no `-dev` configs**: each hardware target has exactly one config, and the branch selects its channel via the single `channel` block at the top of `flake.nix`:
Single branch (`main`) for this repo. The production vs. dev split is at the **flake input + upgrade ref** level:
- `main`: `atticRepo = "radiantwave"`, `gitRef = ""` (default branch).
- `dev`: `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`.
Everything downstream (the configs, ISOs, updater module, autoUpgrade refs) reads from `channel`, so a `dev`→`main` merge carries your changes automatically — no copying between sibling configs. To flip a channel, change only those two `channel` values. When merging, reconcile just this one block.
**No app version is pinned anywhere.** At eval time `flake.nix` fetches the channel's published manifest (`https://files.radiantwavetech.com/<atticRepo>/<manifestFile>`) with `builtins.fetchurl` and bakes its `storePath` verbatim as the app closure. Nix substitutes that exact closure from the channel's Attic cache, so images always match what RadiantWave published and never compile the app from source. Because that fetch is impure (no hash), **every eval that reaches the app needs `--impure`**: CI, local `build-rpi.sh`, and the kiosk's `system.autoUpgrade` (which passes `--impure`). `tarball-ttl = 0` is set in CI and on the kiosks so a rebuild never reuses a stale manifest.
- Production variants consume the `radiantwave` input (`?ref=main`) and auto-upgrade from `KioskConfig.git#<config>` on `main`.
- `-dev` variants consume the `radiantwave-dev` input (`?ref=dev`) and auto-upgrade from `KioskConfig.git?ref=dev#<config>-dev`.
Both gitea repos are public: all git access is plain HTTPS (`git+https://...`), no SSH keys or access tokens on the kiosks. Don't reintroduce `git+ssh` URLs in flake/upgrade refs.
## Key commands
```bash
# Build a system closure (any config name from flake.nix).
# --impure is required: the app closure comes from the channel manifest,
# fetched at eval time (see the prod/dev split above).
nix build --impure .#nixosConfigurations.<name>.config.system.build.toplevel
# Build a system closure (any config name from flake.nix)
nix build .#nixosConfigurations.<name>.config.system.build.toplevel
# Build a bootable ISO
nix build --impure .#nixosConfigurations.<name>.config.system.build.isoImage
nix build .#nixosConfigurations.<name>.config.system.build.isoImage
# Deploy to a remote kiosk over SSH
nixos-rebuild switch --impure --flake .#<name> --target-host root@<host>
nixos-rebuild switch --flake .#<name> --target-host root@<host>
# Push build result to Attic cache (not currently wired into any config)
attic push kiosk ./result
# Evaluate available config names (pure-safe: does not force the app fetch)
# Evaluate available config names
nix eval .#nixosConfigurations --apply builtins.attrNames
```
## Config variants
One config per hardware target. Dev/prod is the branch, not the config — see `channel` in `flake.nix`:
| Name | Target hardware | Disk device | App closure |
| Name | Target hardware | Disk device | App input |
|---|---|---|---|
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | channel manifest `manifest.json` |
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | channel manifest `manifest.json` |
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | channel manifest `manifest.json` |
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | channel manifest `aarch64-manifest.json` |
On the `dev` branch, the same four configs read the `radiantwave-dev` manifests (`?ref=dev`).
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | `radiantwave` (main) |
| `radiantwavekiosk-dev` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | `radiantwave-dev` (dev) |
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | `radiantwave` (main) |
| `radiantwavekiosk-commercial-dev` | Commercial HW (SATA) | `/dev/sda` | `radiantwave-dev` (dev) |
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | `radiantwave` (main) |
| `radiantwavekiosk-android-dev` | Intel NUC 14, rotated display | `/dev/nvme0n1` | `radiantwave-dev` (dev) |
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | `radiantwave` (main) |
| `radiantwavekiosk-rpi-dev` | Raspberry Pi 5 (SD image) | — | `radiantwave-dev` (dev) |
Every variant sets its own `networking.hostName` (same as the config name) and `system.configurationRevision`, so `tools/find_systems.zsh` can identify fleet members.
@@ -62,15 +56,16 @@ Each ISO installs exactly one closure using that closure's disk layout (`mkIso`
| ISO | Installs | Disk layout |
|---|---|---|
| `kiosk-iso` | `radiantwavekiosk` | NVMe (`disk-config.nix`) |
| `dev-iso` | `radiantwavekiosk-dev` | NVMe |
| `commercial-iso` | `radiantwavekiosk-commercial` | SATA (`commercial-disk-config.nix`) |
| `commercial-dev-iso` | `radiantwavekiosk-commercial-dev` | SATA |
| `android-iso` | `radiantwavekiosk-android` | NVMe |
These same names build **dev-flavored** images when the flake is checked out on the `dev` branch (the channel block selects the dev manifest, attic repo, and upgrade ref), so there are no separate `-dev` ISOs.
| `android-dev-iso` | `radiantwavekiosk-android-dev` | NVMe |
The installer lives once in `zero-touch-installer.nix` and is parameterized by `targetConfig`. Adding a new variant = add the `nixosConfigurations.*` entry, a matching `packages.x86_64-linux.*` line, and an ISO if needed.
Raspberry Pi systems are built as SD images, not ISOs:
`nix build --impure .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage` (see `build-rpi.sh`).
`nix build .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage` (see `build-rpi.sh`).
## Hardware facter reports
@@ -90,26 +85,24 @@ Regenerate when major hardware changes, not on a schedule — reports are snapsh
## CI (Gitea)
`.gitea/workflows/build.yml` and `build-rpi.yml` — triggered on pushes to `dev`, on new tags (production), and by `workflow_dispatch`, all on the `orion-lxc` runner. They build and publish exactly the closure the channel manifest announces, so a release is published by RadiantWave CI alone; there is no `flake.lock` bump to make here. Both workflows set `tarball-ttl = 0` and fail fast if the announced closure is not in the channel's Attic cache.
`.gitea/workflows/build-*.yml` — all triggered by `workflow_dispatch` on the `build-server` runner. They only **build and publish** what is committed. `flake.lock` is updated manually: commit an updated lock (e.g. after `nix flake update radiantwave-dev`) and dispatching the workflow is the release trigger.
## Important gotchas
- **Disk device differs by variant**: `disk-config.nix` uses `/dev/nvme0n1`, `commercial-disk-config.nix` uses `/dev/sda`. `mkIso` pairs each ISO with the correct layout automatically — keep new ISOs paired correctly.
- **Changing a config name** requires updating that config's `upgradeFlakeRef` in `flake.nix` (it embeds the name).
- **The `channel` block is the dev/prod toggle** in `flake.nix` — `atticRepo` and `gitRef`. When merging `dev`→`main`, keep this block's values for the target branch; only this block differs between branches.
- **`rotated_display.nix` uses `mkForce`** on `loginShellInit` — it replaces the entire kiosk startup script from `configuration.nix`; don't edit both.
- **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh --impure`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`.
- **Live app updater**: each config's `radiantwaveUpdaterModule` points at a per-arch manifest — x86 uses `<repo>/manifest.json`, RPi uses `<repo>/aarch64-manifest.json` (via the `manifestFile` arg). A 2-min timer polls it and fetches the announced closure from the channel's Attic repo. RPi live updates require RadiantWave CI to publish both an aarch64 closure and `aarch64-manifest.json`; until then the RPi stays on the baked baseline.
- **App closure comes from Attic**: both channels (`radiantwave`, `radiantwave-dev`) are configured as trusted substituters in `configuration.nix`, so the baked app and its updates are substituted, never compiled, on the kiosks. nixpkgs deps still come from cache.nixos.org.
- **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`.
- **No binary cache**: kiosks fetch from cache.nixos.org only and compile RadiantWave itself from source during upgrades. Attic integration was planned (see `PLAN-update-system.md`) but never wired; the old netrc/JWT setup has been removed.
- **Secrets**: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via `nix.settings.access-tokens` in `configuration.nix`.
- **App inputs**: there are none. Do not re-add a pinned `radiantwave` flake input or an `appVersion`; that is exactly what drifted from the published releases. The manifest is the source of truth.
- **Flake inputs**: `radiantwave` tracks main, `radiantwave-dev` tracks `?ref=dev`; prod and `-dev` configs consume them respectively. Updating them independently is intentional.
- **SSH keys on machines are inbound-only** (`authorizedKeys` for admin access); outbound git is HTTPS. The root key in `zero-touch-installer.nix` must stay in sync with `myKey` in `configuration.nix`.
## File map
- `flake.nix` — entry point: inputs, the `channel` dev/prod selector, builders (`mkKioskSystem`, `mkRpiKioskSystem`, `mkIso`), all system definitions, facter report wiring
- `flake.nix` — entry point: inputs, builders (`mkKioskSystem`, `mkRpiKioskSystem`, `mkIso`), all system definitions, facter report wiring
- `configuration.nix` — shared NixOS config (packages, services, kiosk user, auto-upgrade, health check, VM variant)
- `zero-touch-installer.nix` — parameterized USB installer module used by all three ISOs
- `zero-touch-installer.nix` — parameterized USB installer module used by all six ISOs
- `disk-config.nix` — NVMe disk layout (NUC)
- `commercial-disk-config.nix` — SATA disk layout (commercial HW)
- `rotated_display.nix` — portrait display override for Android variant
-19
View File
@@ -1,19 +0,0 @@
Implementation complete and verified. Here's what was done:
Implemented (both repos on dev)
Checkpoints (return points if something fails):
- radiantwave → tag pre-live-update @ d84307b
- KioskConfig → tag pre-live-update @ b8d0f59
radiantwave — .gitea/workflows/dev.yml: Added a "Publish manifest" step after the Attic push. It writes {"version","storePath"} using readlink -f ./result-pkg (the exact closure pushed to the dev Attic), then scps it to 157.245.124.103:/mnt/infrastructure_data/files/radiantwave-dev/manifest.json. main.yml untouched.
KioskConfig:
- New radiantwave-updater.nix module: tmpfiles seed (L /var/lib/radiantwave/current → <baked-in baseline>), a systemd oneshot service (curl manifest → nix copy --from dev Attic, never builds → atomic mv -T swap), and a 2-min persistent timer.
- flake.nix: imports the module in both builders and enables it on all 4 -dev variants (radiantwavekiosk-dev, -commercial-dev, -android-dev, -rpi-dev) pointing at the dev manifest/attic.
- configuration.nix: launches via /var/lib/radiantwave/current/bin/radiantwave when the updater is enabled; prod keeps /run/current-system/sw/bin/radiantwave.
Verified
- All dev configs evaluate with enable=true; prod enable=false, no updater service/timer, login path unchanged.
- Rendered service script correct (escaped shell vars, resoled Nix ínterps); dev login line + tmpfiles seed rule confirmed.
- All 14 nixosConfigurations evaluate; toplevel dry-run of radiantwavekiosk-dev plans cleanly.
- dev.yml parses as valid YAML with the new step.
Before this actually works end-to-end, you need to
1. Create /mnt/infrastructure_data/files/radiantwave-dev/ on the infra host and confirm it's served as https://files.radiantwavetech.com/radiantwave-dev/manifest.json.
2. Commit + push these changes (I staged radiantwave-updater.nix so nix can see it; nothing is committed yet) — push dev.yml change to radiantwave dev, and the KioskConfig changes to dev. The dev-iso/etc. workflows will then bake the seed in.
Note: git add radiantwave-updater.nix was needed for flake evaluation; the KioskConfig changes are staged but uncommitted. Want me to commit them?
Binary file not shown.
+3 -8
View File
@@ -6,19 +6,16 @@ TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
FILENAME="radiantwave-raspberrypi-${TIMESTAMP}.img.zst"
REPO="$(pwd)"
NIX_STORE="kiosk-nix-store"
NIX_STATE="kiosk-nix-state"
echo "==> Building ${CONFIG}..."
echo "==> Output: ${REPO}/${FILENAME}"
echo ""
podman volume create "${NIX_STORE}" 2>/dev/null || true
podman volume create "${NIX_STATE}" 2>/dev/null || true
podman run --rm \
-v "${REPO}:/repo" \
-v "${NIX_STORE}:/nix/store" \
-v "${NIX_STATE}:/nix/var/nix" \
-w /repo \
nixos/nix:latest \
sh -c '
@@ -26,20 +23,18 @@ podman run --rm \
mkdir -p /etc/nix
echo "experimental-features = nix-command flakes" > /etc/nix/nix.conf
echo "accept-flake-config = true" >> /etc/nix/nix.conf
echo "tarball-ttl = 0" >> /etc/nix/nix.conf
echo "extra-substituters = https://cache.nixos.org https://nixos-raspberrypi.cachix.org https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI= radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0=" >> /etc/nix/nix.conf
echo "extra-substituters = https://cache.nixos.org https://nixos-raspberrypi.cachix.org" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=" >> /etc/nix/nix.conf
echo "max-jobs = 4" >> /etc/nix/nix.conf
echo "==> Building sdImage (first run slow, cached after)..."
nix --accept-flake-config build \
.#nixosConfigurations.'${CONFIG}'.config.system.build.sdImage \
--impure \
--fallback \
--print-build-logs
echo "==> Build complete, copying..."
cp result/sd-image/*.img.zst /repo/'${FILENAME}'
cp result/sd-image/*.img* /repo/'${FILENAME}'
echo "==> Done: ${FILENAME}"
ls -lh /repo/'${FILENAME}'
'
+33 -156
View File
@@ -1,31 +1,8 @@
{ config, pkgs, radiantwaveApp, upgradeFlakeRef, radiantwavePlymouth, ... }:
{ pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }:
let
# The published app is a store path with context (builtins.storePath in
# flake.nix), so Nix substitutes it from the channel's Attic cache and the
# system closure depends on it.
radiantApp = radiantwaveApp;
# When the live-updater module is enabled, RadiantWave launches through the
# mutable /var/lib/radiantwave/current
# pointer so it can be swapped without a full nixos-rebuild. Otherwise it
# launches straight from the immutable system closure as before.
launchBin =
if config.services.radiantwaveUpdater.enable or false
then "/var/lib/radiantwave/current/bin/radiantwave"
else "/run/current-system/sw/bin/radiantwave";
radiantApp = radiantwave.packages.${pkgs.system}.default;
myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ];
# Headscale preauth key. Paste/replace here.
#
# This MUST be a *reusable* key with a long expiry (1 year is good), created
# with:
# headscale preauthkeys create --user <user-id> --reusable --expiration 8760h
#
# A single-use key only ever enrolls one node, which would mean a fresh key
# plus a rebuild for every kiosk that joins the fleet. A reusable key lets any
# number of kiosks enroll unattended, and the timer below retries the join so
# a node that misses its window at boot heals on its own.
headscaleAuthKey = "hskey-auth-UfU_eS_sdOu6-CjrqrVdDJFRFsPnaTtK5sFuCiVlffJAS7uQ1wX1_65MNk205WGsF4clOiBD3ZOV-";
in
{
############################################################
@@ -50,7 +27,7 @@ in
# --- RadiantWave splash: Plymouth from boot until the app's first frame ---
boot.plymouth.enable = true;
boot.plymouth.theme = "radiantwave";
boot.plymouth.themePackages = [ (radiantwavePlymouth pkgs) ];
boot.plymouth.themePackages = [ radiantwavePlymouth ];
# Silence the kernel/initrd output behind the splash. The module adds
# "splash" itself; we keep "quiet" plus show_status=false and loglevel 0.
@@ -100,134 +77,11 @@ in
networking.networkmanager.enable = true;
############################################################
# Tailscale (self-hosted Headscale control plane)
############################################################
# The node joins the RadiantWave Headscale server at
# hs.radiantwavetech.com using the *hardware serial* as its Tailscale
# node name (not networking.hostName), so a kiosk is always reachable at
# <serial> even though every variant shares one hostName per branch.
#
# The preauth key is set in `headscaleAuthKey` in the let block at the
# top of this file. NOTE: it ends up in the world-readable /nix/store, so
# keep this file out of public repos while a live key is in it.
services.tailscale = {
enable = true;
useRoutingFeatures = "client";
};
# Reach the kiosk's services (SSH, etc.) over the tailnet without extra
# per-port rules. Membership is gated by the preauth key.
networking.firewall.trustedInterfaces = [ "tailscale0" ];
systemd.services.radiantwave-tailscale-auth = {
description = "Join the RadiantWave tailnet using the hardware serial as node name";
after = [ "tailscaled.service" "network-online.target" ];
wants = [ "tailscaled.service" "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
# Deliberately no RemainAfterExit: the unit has to fall back to inactive
# so the timer below can trigger it again. The script is idempotent, so
# re-running it on an already-joined kiosk is a cheap no-op.
};
path = [ pkgs.tailscale pkgs.coreutils pkgs.gnused pkgs.gnugrep ];
script = ''
set -eu
# Hardware serial. x86 exposes it via DMI; Raspberry Pi exposes it in
# the device tree (null-terminated), mirrored under /proc/device-tree.
serial=""
for f in \
/sys/class/dmi/id/product_serial \
/sys/class/dmi/id/board_serial \
/sys/firmware/devicetree/base/serial-number \
/proc/device-tree/serial-number
do
if [ -r "$f" ]; then
serial="$(tr -d '\000' < "$f" | tr -d '\n\r')"
[ -n "$serial" ] && break
fi
done
# Tailscale node names are RFC 1035 labels: lowercase, [a-z0-9-], <=63.
serial="$(printf '%s' "$serial" \
| tr '[:upper:]' '[:lower:]' \
| tr -c 'a-z0-9-' '-' \
| sed -e 's/-\{1,\}/-/g' -e 's/^-//' -e 's/-$//' \
| cut -c1-63)"
[ -n "$serial" ] || serial="radiantwave-unknown"
# Already joined? Bail out early. This is what makes the periodic
# re-invocation from the timer below cheap and safe to run forever.
# The JSON is pretty-printed with a space, so allow optional whitespace.
state="$(tailscale status --json 2>/dev/null \
| sed -n 's/.*"BackendState": *"\([A-Za-z]*\)".*/\1/p')"
if [ "$state" = "Running" ]; then
echo "radiantwave-tailscale-auth: already joined as '$serial'"
exit 0
fi
echo "radiantwave-tailscale-auth: joining as '$serial' (state: ''${state:-unknown})"
for _ in $(seq 1 30); do
if tailscale up \
--auth-key=${pkgs.lib.escapeShellArg headscaleAuthKey} \
--login-server=https://hs.radiantwavetech.com \
--hostname="$serial" \
--accept-dns=false
then
echo "radiantwave-tailscale-auth: connected"
exit 0
fi
sleep 2
done
# Best-effort by design: do NOT exit non-zero here.
#
# This unit is part of the new generation that switch-to-configuration
# starts, and *any* failing unit among those aborts the switch with
# status 4/NOPERMISSION. While the preauth key was expired that is exactly
# what happened: this unit failed, which failed `switch-to-configuration`,
# which failed nixos-upgrade.service -- so the tailscale key silently
# stopped *all* system upgrades on the fleet every 10 minutes.
#
# The retry timer below is what actually recovers the join, so a failed
# attempt here must never take down the system switch with it.
echo "radiantwave-tailscale-auth: could not join tailnet after retries;" >&2
echo "radiantwave-tailscale-auth: leaving the join to the retry timer" >&2
exit 0
'';
};
# The join only needs to happen once per node, but it fails for reasons
# outside our control: the network may not be usable yet at boot, tailscaled
# may be restarted later, or the preauth key may be rotated out from under
# the fleet. A timer re-runs the (idempotent) join so a kiosk recovers on its
# own instead of needing a manual `systemctl restart`.
#
# This replaces the old behavior where the one-shot unit failed once at boot
# and stayed failed forever (Restart=no, NRestarts=0).
systemd.timers.radiantwave-tailscale-auth = {
description = "Periodically ensure the kiosk is joined to the tailnet";
wantedBy = [ "timers.target" ];
timerConfig = {
# The join is attempted at boot via multi-user.target above; this is the
# first retry in case that one lost the race with the network.
OnBootSec = "5min";
# Measured from when the unit last went inactive, so a slow join doesn't
# immediately retrigger.
OnUnitInactiveSec = "10min";
Unit = "radiantwave-tailscale-auth.service";
};
};
system.autoUpgrade = {
enable = true;
flake = upgradeFlakeRef;
dates = "*:00/10";
# --impure: the flake reads the channel's published manifest at eval time
# (builtins.fetchurl) to pick the app store path for the next generation.
flags = [ "--refresh" "--impure" "-L" ];
flags = [ "--refresh" "-L" ];
operation = "switch";
};
@@ -254,11 +108,6 @@ in
# NixOS settings.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.accept-flake-config = true;
# The app baked into each generation comes from the channel's published
# manifest, fetched at eval time with `builtins.fetchurl` (see flake.nix).
# Disable the tarball cache so a rebuild during autoUpgrade always reads the
# current manifest instead of a copy up to an hour old.
nix.settings.tarball-ttl = 0;
nix.settings.extra-substituters = [
"https://attic.radiantwavetech.com/radiantwave"
"https://attic.radiantwavetech.com/radiantwave-dev"
@@ -310,7 +159,7 @@ in
# Release the splash screen right before the compositor takes over the display
sudo -n plymouth quit --wait || true
exec dbus-run-session cage -- ${launchBin} -d -s > /tmp/kiosk.log 2>&1
exec dbus-run-session cage -- /run/current-system/sw/bin/radiantwave -d -s > /tmp/kiosk.log 2>&1
fi
'';
@@ -406,4 +255,32 @@ in
echo "Backup completed at $(date)" > "$BACKUP_DIR/.backup-timestamp"
'';
};
# --- Health check service: runs after nixos-upgrade ---
# systemd.services.radiantwave-healthcheck = {
# description = "Verify RadiantWave is running after upgrade, rollback if not";
# after = [ "nixos-upgrade.service" ];
# wantedBy = [ "nixos-upgrade.service" ];
# serviceConfig = {
# Type = "oneshot";
# # Give cage time to start radiantwave after the switch
# ExecStartPre = "${pkgs.coreutils}/bin/sleep 90";
# };
# script = ''
# # The installed `radiantwave` is a makeWrapper script that execs the real
# # binary (`.radiantwave-unwrapped`), so the process comm is not exactly
# # `radiantwave`; match the exec'd binary's command line instead.
# if ${pkgs.procps}/bin/pgrep -f '\.radiantwave-unwrapped' > /dev/null; then
# echo "RadiantWave is running, upgrade successful"
# exit 0
# fi
# echo "RadiantWave not found after upgrade, rolling back..."
# # Roll back to previous NixOS generation
# /run/current-system/sw/bin/nixos-rebuild switch --rollback
# echo "Rollback completed at $(date)"
# '';
# };
}
-74
View File
@@ -1,74 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.kioskEdid;
overrideModule = types.submodule {
options = {
connector = mkOption {
type = types.str;
example = "HDMI-A-1";
description = ''
DRM connector name to apply the EDID override to.
Check `ls /sys/class/drm/` on target hardware to confirm the
exact name (e.g. HDMI-A-1, HDMI-A-2).
'';
};
edidFile = mkOption {
type = types.path;
description = ''
Path to the binary EDID file to force for this connector.
'';
};
forceConnected = mkOption {
type = types.bool;
default = true;
description = ''
Also force this connector to be treated as connected
(video=<connector>:e), in case the real link reports
disconnected/garbled state through a splitter or wireless kit.
'';
};
};
};
in
{
options.services.kioskEdid = {
enable = mkEnableOption "forcing custom EDIDs on one or more display connectors";
overrides = mkOption {
type = types.listOf overrideModule;
default = [ ];
description = ''
List of per-connector EDID overrides to apply. Each entry gets its
own firmware file and its own drm.edid_firmware kernel param.
'';
example = literalExpression ''
[
{ connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; }
{ connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; }
]
'';
};
};
config = mkIf cfg.enable {
hardware.firmware = map
(o: pkgs.runCommand "kiosk-edid-firmware-${o.connector}" { } ''
mkdir -p $out/lib/firmware/edid
cp ${o.edidFile} $out/lib/firmware/edid/${o.connector}.bin
'')
cfg.overrides;
boot.kernelParams = flatten (map
(o:
[ "drm.edid_firmware=${o.connector}:edid/${o.connector}.bin" ]
++ optional o.forceConnected "video=${o.connector}:e"
)
cfg.overrides);
};
}
Generated
+141 -1
View File
@@ -51,6 +51,42 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixos-facter-modules": {
"locked": {
"lastModified": 1773858690,
@@ -146,12 +182,116 @@
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1785967620,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1785967620,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"radiantwave": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1787522765,
"narHash": "sha256-D3KOrGcAH07lvKYgyiwejOQpDW91js7zI6l87WdabLk=",
"ref": "main",
"rev": "63265cc22f3055a782a0114e92c57a36bef723b2",
"revCount": 40,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
}
},
"radiantwave-dev": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1787522765,
"narHash": "sha256-D3KOrGcAH07lvKYgyiwejOQpDW91js7zI6l87WdabLk=",
"ref": "dev",
"rev": "63265cc22f3055a782a0114e92c57a36bef723b2",
"revCount": 40,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
"original": {
"ref": "dev",
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
}
},
"root": {
"inputs": {
"disko": "disko",
"nixos-facter-modules": "nixos-facter-modules",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs_2",
"radiantwave": "radiantwave",
"radiantwave-dev": "radiantwave-dev"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
+70 -115
View File
@@ -4,6 +4,8 @@
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
radiantwave.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=main";
radiantwave-dev.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=dev";
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
};
@@ -13,6 +15,8 @@
nixpkgs,
disko,
nixos-facter-modules,
radiantwave,
radiantwave-dev,
nixos-raspberrypi,
...
}@inputs:
@@ -24,12 +28,11 @@
# at build time (librsvg), so the theme is hermetic and resolution-
# independent. The .plymouth file's /usr paths are rewritten to $out
# (the NixOS module's initrd fixup rewrites store paths again).
mkRadiantwavePlymouth = targetPkgs:
targetPkgs.stdenv.mkDerivation {
radiantwavePlymouth = pkgs.stdenv.mkDerivation {
pname = "radiantwave-plymouth";
version = "1.0.0";
nativeBuildInputs = [ targetPkgs.librsvg ];
nativeBuildInputs = [ pkgs.librsvg ];
src = ./assets/RWT-Circular-Color-ForOnDark.svg;
dontUnpack = true;
@@ -52,77 +55,9 @@
revision = self.rev or self.dirtyRev or null;
# DEV/PROD CHANNEL SELECTOR — the single block that differs between the
# `main` and `dev` branches. Everything downstream (the four configs, the
# ISOs, the updater module) is identical on both branches and pulls from
# this channel, so a `dev`-to-`main` merge carries your changes with it
# automatically. There are no more `-dev` sibling configs to copy.
#
# main: atticRepo = "radiantwave", gitRef = ""
# dev: atticRepo = "radiantwave-dev", gitRef = "?ref=dev"
#
# To flip branches, change ONLY these two values (then push/merge).
#
# Note there is deliberately NO app version here. The RadiantWave app is
# not built or pinned by this repo; each channel's published manifest is
# the single source of truth (see `publishedApp` below).
channel = {
atticRepo = "radiantwave-dev"; # dev
gitRef = "?ref=dev"; # dev
};
# Exposed so CI (see .gitea/workflows/build.yml) and humans can verify
# which channel this checkout resolves to. On `main` this is
# "radiantwave"; on `dev` it is "radiantwave-dev".
channelName = channel.atticRepo;
# Channel metadata shared by the configs and the CI preflight.
channelAtticUrl = "https://attic.radiantwavetech.com/${channel.atticRepo}";
channelManifestDir = "https://files.radiantwavetech.com/${channel.atticRepo}";
# The baked RadiantWave app is whatever the channel's published manifest
# currently announces. RadiantWave CI builds the app, pushes the closure
# to the channel's Attic cache, and writes {version, storePath} to the
# manifest. We fetch that manifest at eval time and use its storePath
# verbatim, so an image always carries exactly the published closure and
# never compiles RadiantWave from source. This is an *impure* fetch (no
# hash), so every eval that reaches the app needs `--impure`: CI, the
# kiosk's system.autoUpgrade, and local `build-rpi.sh` all pass it.
manifestFor = manifestFile:
builtins.fromJSON (builtins.readFile (builtins.fetchurl {
url = "${channelManifestDir}/${manifestFile}";
}));
# The published closure as a plain store-path string. Used for the
# updater's runtime baseline pointer (a plain string; the closure itself
# is pulled in via publishedApp below).
publishedPath = manifestFile: (manifestFor manifestFile).storePath;
# The published closure as a real store-path dependency.
# builtins.storePath makes the derivation depend on this exact path and
# substitute it from the channel's Attic cache (configured as a trusted
# substituter), so the app is never compiled from source. The closure
# itself is not fetched by this repository; RadiantWave CI publishes it.
publishedApp = manifestFile: builtins.storePath (publishedPath manifestFile);
# Live app-update side channel. The manifest is served by the
# fileserver; closures are fetched (never built) from the channel's
# Attic cache. `repo` selects the channel — "radiantwave" (main) or
# "radiantwave-dev" (dev) — and `manifestFile` selects the per-arch
# manifest (default "manifest.json"; Raspberry Pi uses
# "aarch64-manifest.json"). All variants follow this same update
# procedure; only the channel/arch differ.
radiantwaveUpdaterModule = { repo, baseline, manifestFile ? "manifest.json" }: {
services.radiantwaveUpdater = {
enable = true;
baselinePath = baseline;
manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}";
atticUrl = "https://attic.radiantwavetech.com/${repo}";
};
};
mkKioskSystem = {
system,
radiantwave,
upgradeFlakeRef,
hostName,
diskModule,
@@ -132,9 +67,9 @@
inherit system;
specialArgs = {
inherit radiantwave;
inherit upgradeFlakeRef;
radiantwaveApp = publishedApp "manifest.json";
radiantwavePlymouth = mkRadiantwavePlymouth;
inherit radiantwavePlymouth;
};
modules = [
@@ -144,7 +79,6 @@
system.configurationRevision = revision;
}
./configuration.nix
./radiantwave-updater.nix
disko.nixosModules.disko
diskModule
nixos-facter-modules.nixosModules.facter
@@ -155,24 +89,24 @@
# facter-<model>.json, and select per variant here, e.g.:
# { facter.reportPath = ./facter-commercial.json; }
{ facter.reportPath = ./facter.json; }
radiantwave.nixosModules.default
] ++ extraModules;
};
mkRpiKioskSystem = {
radiantwave,
upgradeFlakeRef,
hostName,
extraModules ? [],
}:
nixos-raspberrypi.lib.nixosSystem {
specialArgs = {
inherit upgradeFlakeRef;
inherit radiantwave upgradeFlakeRef;
inherit nixos-raspberrypi;
radiantwaveApp = publishedApp "aarch64-manifest.json";
radiantwavePlymouth = mkRadiantwavePlymouth;
inherit radiantwavePlymouth;
};
modules = [
{ nixpkgs.config.allowUnfree = true; }
({ ... }: {
imports = with nixos-raspberrypi.nixosModules; [
sd-image
@@ -182,7 +116,7 @@
];
})
./configuration.nix
./radiantwave-updater.nix
radiantwave.nixosModules.default
{
networking.hostName = hostName;
system.configurationRevision = revision;
@@ -232,73 +166,82 @@
};
in
{
inherit channelName channelAtticUrl channelManifestDir;
packages.x86_64-linux.radiantwavekiosk = self.nixosConfigurations.radiantwavekiosk.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-dev = self.nixosConfigurations.radiantwavekiosk-dev.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-commercial = self.nixosConfigurations.radiantwavekiosk-commercial.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-commercial-dev = self.nixosConfigurations.radiantwavekiosk-commercial-dev.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-android = self.nixosConfigurations.radiantwavekiosk-android.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-android-dev = self.nixosConfigurations.radiantwavekiosk-android-dev.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-rpi = self.nixosConfigurations.radiantwavekiosk-rpi.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-rpi-dev = self.nixosConfigurations.radiantwavekiosk-rpi-dev.config.system.build.toplevel;
# Kiosk systems — one per hardware target, no dev/prod siblings.
# Kiosk systems.
#
# Every config consumes the channel selected at the top of this file
# (see `channel`): the published manifest whose closure is baked as the
# app, the Attic repo/manifest it fetches live updates from, and the
# KioskConfig git ref it auto-upgrades from. On `main` the channel is
# prod; on `dev` it is the dev channel. Because there is exactly one
# config per hardware type, changes made on `dev` reach prod via a plain
# merge — no copying.
# Production variants consume the `radiantwave` input (?ref=main);
# `-dev` variants consume `radiantwave-dev` (?ref=dev) and track the
# dev branch of this repo via their autoUpgrade flake refs.
nixosConfigurations = {
radiantwavekiosk = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
hostName = "radiantwavekiosk";
diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk";
extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; })
];
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk";
};
radiantwavekiosk-dev = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave-dev;
hostName = "radiantwavekiosk-dev";
diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-dev";
};
radiantwavekiosk-commercial = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
hostName = "radiantwavekiosk-commercial";
diskModule = ./commercial-disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial";
extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; })
];
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-commercial";
};
radiantwavekiosk-commercial-dev = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave-dev;
hostName = "radiantwavekiosk-commercial-dev";
diskModule = ./commercial-disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-commercial-dev";
};
# Rotated screen for android-style displays.
radiantwavekiosk-android = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
hostName = "radiantwavekiosk-android";
diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android";
extraModules = [
./rotated_display.nix
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; })
];
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android";
extraModules = [./rotated_display.nix];
};
radiantwavekiosk-android-dev = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave-dev;
hostName = "radiantwavekiosk-android-dev";
diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev";
extraModules = [./rotated_display.nix];
};
# Raspberry Pi 5.
radiantwavekiosk-rpi = mkRpiKioskSystem {
radiantwave = radiantwave;
hostName = "radiantwavekiosk-rpi";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi";
extraModules = [
(radiantwaveUpdaterModule {
repo = channel.atticRepo;
baseline = publishedPath "aarch64-manifest.json";
manifestFile = "aarch64-manifest.json";
})
];
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-rpi";
};
radiantwavekiosk-rpi-dev = mkRpiKioskSystem {
radiantwave = radiantwave-dev;
hostName = "radiantwavekiosk-rpi-dev";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-rpi-dev";
};
# === USB Images ===
# Each ISO installs exactly the named system closure using that
# system's disk layout. NVMe layouts pair with ./disk-config.nix,
# SATA with ./commercial-disk-config.nix — don't cross them.
# The same names build dev-flavored images when the flake is checked
# out on the `dev` branch (the channel block above selects the dev
# manifest, attic repo, and upgrade ref), so there are no separate
# -dev ISOs.
kiosk-iso = mkIso {
targetConfig = "radiantwavekiosk";
diskModule = ./disk-config.nix;
@@ -311,6 +254,18 @@
targetConfig = "radiantwavekiosk-android";
diskModule = ./disk-config.nix;
};
dev-iso = mkIso {
targetConfig = "radiantwavekiosk-dev";
diskModule = ./disk-config.nix;
};
commercial-dev-iso = mkIso {
targetConfig = "radiantwavekiosk-commercial-dev";
diskModule = ./commercial-disk-config.nix;
};
android-dev-iso = mkIso {
targetConfig = "radiantwavekiosk-android-dev";
diskModule = ./disk-config.nix;
};
};
};
}
-128
View File
@@ -1,128 +0,0 @@
#!/usr/bin/env python3
"""
Forces 2160x3840 portrait @ 30Hz as the ONLY Detailed Timing Descriptor
(DTD slot 1 / preferred timing) and strips the Video Data Block from the
CTA-861 extension so no other video mode is advertised in any way.
Usage:
python3 force_4k30_portrait_only.py donor.bin fixed.bin
"""
import sys
def build_dtd(pixel_clock_khz, h_active, h_blank, h_sync_offset, h_sync_width,
v_active, v_blank, v_sync_offset, v_sync_width,
h_size_mm=0, v_size_mm=0, h_border=0, v_border=0,
interlaced=False, hsync_pos=True, vsync_pos=True):
pc = round(pixel_clock_khz / 10) # EDID stores pixel clock in 10kHz units
dtd = bytearray(18)
dtd[0] = pc & 0xFF
dtd[1] = (pc >> 8) & 0xFF
dtd[2] = h_active & 0xFF
dtd[3] = h_blank & 0xFF
dtd[4] = ((h_active >> 8) << 4) | (h_blank >> 8)
dtd[5] = v_active & 0xFF
dtd[6] = v_blank & 0xFF
dtd[7] = ((v_active >> 8) << 4) | (v_blank >> 8)
dtd[8] = h_sync_offset & 0xFF
dtd[9] = h_sync_width & 0xFF
dtd[10] = ((v_sync_offset & 0xF) << 4) | (v_sync_width & 0xF)
dtd[11] = (((h_sync_offset >> 8) & 0x3) << 6) | \
(((h_sync_width >> 8) & 0x3) << 4) | \
(((v_sync_offset >> 4) & 0x3) << 2) | \
((v_sync_width >> 4) & 0x3)
dtd[12] = h_size_mm & 0xFF
dtd[13] = v_size_mm & 0xFF
dtd[14] = ((h_size_mm >> 8) << 4) | (v_size_mm >> 8)
dtd[15] = h_border
dtd[16] = v_border
flags = 0x18 # digital separate sync
if interlaced:
flags |= 0x80
if hsync_pos:
flags |= 0x02
if vsync_pos:
flags |= 0x04
dtd[17] = flags
return dtd
TAG_VIDEO = 2
def strip_video_data_block(ext):
"""Remove all Video Data Blocks from a CTA-861 extension block."""
dtd_offset = ext[2]
pos = 4 # skip 4-byte CTA header
blocks = []
while pos < dtd_offset:
tag = (ext[pos] >> 5) & 0x07
length = ext[pos] & 0x1F
block_data = bytes(ext[pos + 1:pos + 1 + length])
blocks.append((tag, length, block_data))
pos += 1 + length
blocks = [(t, l, d) for t, l, d in blocks if t != TAG_VIDEO]
new_db = bytearray()
for tag, length, data in blocks:
new_db.append((tag << 5) | length)
new_db.extend(data)
new_ext = bytearray(128)
new_ext[0] = ext[0] # CTA tag
new_ext[1] = ext[1] # revision
new_ext[2] = 4 + len(new_db) # new DTD offset
new_ext[3] = ext[3] # flags (audio, YCbCr, etc.)
new_ext[4:4 + len(new_db)] = new_db
new_ext[0x7F] = 0
new_ext[0x7F] = (-sum(new_ext)) % 256
return new_ext
if __name__ == "__main__":
infile, outfile = sys.argv[1], sys.argv[2]
data = bytearray(open(infile, "rb").read())
# --- 2160x3840 portrait @ 30Hz ---
# H blanking uses CVT-RB-style fixed values (front 48 / sync 32 / back 80)
# V blanking computed to satisfy CVT-RB min vertical blank time (~460us)
# Pixel clock ~= htotal * vtotal * refresh
dtd_4k30 = build_dtd(
pixel_clock_khz=271020,
h_active=2160, h_blank=160, h_sync_offset=48, h_sync_width=32,
v_active=3840, v_blank=54, v_sync_offset=3, v_sync_width=5,
h_size_mm=685, v_size_mm=1218, # swapped panel physical size for portrait
)
# Slot 1 = bytes 54-71 (was 1920x1080@60, the preferred timing)
data[54:72] = dtd_4k30
# Slot 2 = bytes 72-89 (was 1280x720@60) - blank out to a dummy
# descriptor so no second timing is advertised at all.
# Dummy Descriptor format: 00 00 00 10 00, then 13 zero-padding bytes.
dummy = bytearray(18)
dummy[3] = 0x10 # tag byte for "Dummy Descriptor"
data[72:90] = dummy
# Unknown Display Descriptor slot and Product Name descriptor are
# left as-is.
# Recompute base block checksum (byte 127)
data[127] = (256 - sum(data[0:127]) % 256) % 256
# Strip Video Data Block from CTA-861 extension (bytes 128-255)
data[128:256] = strip_video_data_block(data[128:256])
open(outfile, "wb").write(data)
print(f"Wrote {outfile}")
print("Run `edid-decode` on the output to verify before flashing.")
-98
View File
@@ -1,98 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.radiantwaveUpdater;
in
{
options.services.radiantwaveUpdater = {
enable = lib.mkEnableOption "live RadiantWave app updates via a polled manifest";
manifestUrl = lib.mkOption {
type = lib.types.str;
description = "HTTP(S) URL of the JSON manifest announcing the current app store path.";
};
atticUrl = lib.mkOption {
type = lib.types.str;
description = "Attic cache URL to fetch the app closure from (fetch-only, never builds).";
};
baselinePath = lib.mkOption {
type = lib.types.str;
description = "Store path of the RadiantWave closure baked into the system closure (from the channel manifest), used to seed the runtime pointer before the first update.";
};
};
config = lib.mkIf cfg.enable {
# Runtime pointer the login shell launches RadiantWave from. Seeded on
# first boot to the exact closure the channel manifest announced (the same
# path the updater would fetch), so no spurious first update is triggered.
systemd.tmpfiles.rules = [
"L /var/lib/radiantwave/current - - - - ${cfg.baselinePath}"
];
systemd.services.radiantwave-updater = {
description = "Poll the RadiantWave manifest and swap the live app pointer";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
# NOTE: no RemainAfterExit here. Keeping the oneshot "active" after
# it finishes breaks the timer's OnUnitActiveSec re-arming, so the
# 2-min cadence stops after the first run (NEXT goes empty, Trigger
# becomes n/a). Let the service return to dead so the timer refires.
};
script = ''
set -u
NEW_PATH=$(${pkgs.curl}/bin/curl -fsSL "${cfg.manifestUrl}" \
| ${pkgs.jq}/bin/jq -er .storePath) || {
echo "radiantwave-updater: failed to fetch/parse manifest from ${cfg.manifestUrl}"
exit 0
}
CURRENT=$(${pkgs.coreutils}/bin/readlink /var/lib/radiantwave/current 2>/dev/null || true)
if [ -z "$NEW_PATH" ] || [ "$NEW_PATH" = "$CURRENT" ]; then
# Already current; make sure the running closure is GC-rooted.
${pkgs.coreutils}/bin/ln -sfn "$CURRENT" /var/lib/radiantwave/.current-root 2>/dev/null || true
echo "radiantwave-updater: already current (''${CURRENT})"
exit 0
fi
# Realize the closure via the variant's Attic cache plus cache.nixos.org.
# nixpkgs deps (e.g. sqlite) are signed by cache.nixos.org and Attic
# intentionally skips them to avoid re-uploading the world, so fetch
# them from cache.nixos.org and the whole closure resolves without ever
# building from source. Trusted public keys come from the system config
# (both Attic repos are trusted in configuration.nix). --add-root pins
# the fetched closure so GC can't collect it while the swap below makes
# it live.
echo "radiantwave-updater: acquiring ''${NEW_PATH}"
${pkgs.nix}/bin/nix-store -r "$NEW_PATH" \
--add-root /var/lib/radiantwave/.current-root \
--option extra-substituters "${cfg.atticUrl} https://cache.nixos.org" || {
echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}"
exit 1
}
# Atomic swap: the app never observes a half-updated pointer.
${pkgs.coreutils}/bin/ln -sfn "$NEW_PATH" /var/lib/radiantwave/current.tmp
${pkgs.coreutils}/bin/mv -T /var/lib/radiantwave/current.tmp /var/lib/radiantwave/current
echo "radiantwave-updater: swapped ''${CURRENT} -> ''${NEW_PATH}"
loginctl terminate-user kiosk
'';
};
systemd.timers.radiantwave-updater = {
description = "Check for RadiantWave app updates";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "2min";
Persistent = true;
Unit = "radiantwave-updater.service";
};
};
};
}
+3 -10
View File
@@ -1,13 +1,6 @@
{ config, pkgs, radiantwaveApp, ...}:
{ pkgs, radiantwave, ...}:
let
radiantApp = radiantwaveApp;
# Match configuration.nix: when the live updater is enabled, launch through
# the mutable /var/lib/radiantwave/current pointer so Android kiosks receive
# app updates instead of always running the baked baseline.
launchBin =
if config.services.radiantwaveUpdater.enable or false
then "/var/lib/radiantwave/current/bin/radiantwave"
else "${radiantApp}/bin/radiantwave";
radiantApp = radiantwave.packages.${pkgs.system}.default;
kioskscript = pkgs.writeShellScript "kiosk-start" ''
# Wait for the Wayland display to be ready
for i in {1..10}; do
@@ -20,7 +13,7 @@ let
OUTPUT=$(${pkgs.wlr-randr}/bin/wlr-randr | grep -m1 '^\S' | awk '{print $1}')
echo "Rotating output: $OUTPUT" >> /tmp/kiosk.log
${pkgs.wlr-randr}/bin/wlr-randr --output "$OUTPUT" --transform 90 >> /tmp/kiosk.log 2>&1 || echo "wlr-randr failed" >> /tmp/kiosk.log
exec ${launchBin} -d -s
exec ${radiantApp}/bin/radiantwave -d -s
'';
in
{
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -9,7 +9,7 @@
set -uo pipefail # no -e: we want to keep going past unreachable hosts
SUBNET="${1:-192.168.1.0/24}"
SUBNET="${1:-192.168.4.0/24}"
SSH_USER="kiosk"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new)