29 Commits
Author SHA1 Message Date
NeilClack e50391ba9e ci: unify all kiosk build jobs into one branch-aware workflow
Build Kiosk Images / android (push) Canceled after 0s
Build Kiosk Images / commercial (push) Canceled after 0s
Build Kiosk Images / kiosk (push) Canceled after 0s
Build Kiosk Images / rpi (push) Canceled after 8m21s
Single .gitea/workflows/build.yml shared by both branches; the checked-out
ref selects the channel, so no per-branch copies.

- dev push -> builds all development images (kiosk, commercial, android, rpi)
- new tag  -> builds all production images; manual dispatch also supported
- Raspberry Pi sdImage built on orion-lxc via aarch64 emulation
- production-only baseline preflight (skipped on the dev channel)
- artifact names standardized to radiantwave-<variant>-<prod|dev>-<timestamp>
2026-10-04 18:04:35 -05:00
NeilClack 8796feddd9 Declare app version per channel and fix Android live updates
- Add channel.appVersion and an appPackage override so pure builds bake the pinned version instead of RadiantWave's 0.1.0 default.

- Pin the radiantwave input to tag 1.8 and allow unfree in the RPi builder to match x86.

- radiantwaveUpdaterModule gains a manifestFile arg; Raspberry Pi polls aarch64-manifest.json.

- Launch the rotated/Android kiosk through /var/lib/radiantwave/current so the live updater actually applies.
2026-10-03 11:59:29 -05:00
NeilClack b6de748f8f Replace radiantwave-infra with IP address
Build Dev ISOs / build-dev-kiosk (push) Successful in 1m20s
Build Dev ISOs / build-dev-android (push) Successful in 1m36s
Build Dev ISOs / build-dev-commercial (push) Successful in 3m47s
2026-09-30 14:25:42 -05:00
NeilClack cac2e455ec Update output file path
Build Dev ISOs / build-dev-kiosk (push) Failing after 13s
Build Dev ISOs / build-dev-android (push) Failing after 12s
Build Dev ISOs / build-dev-commercial (push) Failing after 11s
2026-09-30 12:27:04 -05:00
NeilClack 85788ce32d Update action runner to orion-lxc
Build Dev ISOs / build-dev-kiosk (push) Failing after 1s
Build Dev ISOs / build-dev-android (push) Failing after 1s
Build Dev ISOs / build-dev-commercial (push) Failing after 1s
2026-09-30 12:04:51 -05:00
NeilClack e6138d398d Update raspberry pi builds 2026-09-30 12:04:37 -05:00
NeilClack c18e158b69 Point dev branch at the dev RadiantWave channel
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m29s
Build Dev ISOs / build-dev-android (push) Successful in 4m31s
Build Dev ISOs / build-dev-commercial (push) Successful in 4m36s
Fixes b136d17 which left the channel block on main values, so the dev
branch was building/tracking prod instead of radiantwave-dev.
2026-08-28 20:30:51 -05:00
NeilClack b136d1751b Merge -dev closures.
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m55s
Build Dev ISOs / build-dev-android (push) Successful in 4m52s
Build Dev ISOs / build-dev-commercial (push) Successful in 4m53s
Consolidates 8 closures to only 4 by using a git branch selector that sets
the values for -dev variants of each closure.

Reference the 'Verifying KioskConfig builds are in the right branch' note
in Obsidian for more reference.
2026-08-28 20:15:32 -05:00
NeilClack 05bb98b107 Enable live app updater on production variants
Build Dev ISOs / build-dev-android (push) Canceled after 0s
Build Dev ISOs / build-dev-commercial (push) Canceled after 0s
Build Dev ISOs / build-dev-kiosk (push) Canceled after 1m10s
All variants now follow the same update procedure as the dev flakes;
prod variants pull the RadiantWave binary, Attic repo, and manifest
from the radiantwave channel instead of radiantwave-dev.
2026-08-28 16:37:13 -05:00
NeilClack ca89c234ab Fix updater timer not re-arming after first run
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m32s
Build Dev ISOs / build-dev-commercial (push) Successful in 5m4s
Build Dev ISOs / build-dev-android (push) Successful in 5m28s
RemainAfterExit=true kept the oneshot 'active' forever, which broke the
timer's OnUnitActiveSec re-arming: the timer fired once (NEXT emptied,
Trigger n/a) and never polled again. Remove it so the service returns to
dead and the 2-min cadence keeps firing.
2026-08-28 15:59:23 -05:00
NeilClack 9a8a258228 GC-root the fetched radiantwave closure
Build Dev ISOs / build-dev-kiosk (push) Canceled after 0s
Build Dev ISOs / build-dev-android (push) Canceled after 0s
Build Dev ISOs / build-dev-commercial (push) Canceled after 0s
nix-store -r was fetching the closure without --add-root, so the swapped-in
app pointer wasn't a GC root and the running closure could be collected.
Pin it with --add-root .current-root, and root the already-current closure
on the no-op path too.
2026-08-28 15:30:20 -05:00
NeilClack 7d8cc56ba0 Fetch updater closures from cache.nixos.org as well as Attic
Build Dev ISOs / build-dev-kiosk (push) Successful in 5m1s
Build Dev ISOs / build-dev-android (push) Successful in 4m44s
Build Dev ISOs / build-dev-commercial (push) Canceled after 2m31s
nix copy --from <attic> only pulls from that single store, so nixpkgs
deps (e.g. sqlite) that Attic skips and leaves on cache.nixos.org were
missing -> 'path is not valid'. Realize the closure through the machine's
substituters so the app comes from the dev Attic and its upstream deps
resolve from cache.nixos.org, without building from source.
2026-08-28 15:25:04 -05:00
NeilClack e44626feec Update RadiantWave a9087f2
Build Dev ISOs / build-dev-kiosk (push) Successful in 5m38s
Build Dev ISOs / build-dev-android (push) Successful in 5m21s
Build Dev ISOs / build-dev-commercial (push) Successful in 5m38s
2026-08-28 13:55:59 -05:00
NeilClack 48c31d0f0c Move home systems to first build slot
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m56s
Build Dev ISOs / build-dev-android (push) Successful in 4m45s
Build Dev ISOs / build-dev-commercial (push) Successful in 5m10s
2026-08-27 21:26:45 -05:00
NeilClack d661162676 Add live RadiantWave updater (dev variants)
Build Dev ISOs / build-dev-android (push) Successful in 1m48s
Build Dev ISOs / build-dev-kiosk (push) Successful in 1m37s
Build Dev ISOs / build-dev-commercial (push) Successful in 1m46s
Add a radiantwave-updater NixOS module that polls a manifest on the
fileserver and swaps the app pointer /var/lib/radiantwave/current to new
closures fetched from the dev Attic cache. Closures are fetched with
'nix copy --from', never built from source; the swap is atomic via
mv -T.

Enable it on the -dev variants only for now: the login shell launches
from /var/lib/radiantwave/current/bin/radiantwave when enabled, and from
the immutable system path otherwise, so prod behavior is unchanged. The
pointer is seeded on first boot to the baseline copy already baked into
the closure (no duplication).
2026-08-27 20:58:07 -05:00
NeilClack b8d0f59c0a Remove EDID overwrite
Build Dev ISOs / build-dev-android (push) Successful in 4m50s
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m49s
Build Dev ISOs / build-dev-commercial (push) Successful in 4m45s
2026-08-26 12:30:36 -05:00
NeilClack 4e36113636 Disable EDID Override
Build Dev ISOs / build-dev-android (push) Successful in 4m53s
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m51s
Build Dev ISOs / build-dev-commercial (push) Successful in 4m45s
2026-08-26 11:38:17 -05:00
NeilClack 2bf762acac Add actual EDID from Android Kiosk
Build Dev ISOs / build-dev-android (push) Successful in 5m5s
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m56s
Build Dev ISOs / build-dev-commercial (push) Successful in 4m53s
2026-08-26 10:12:45 -05:00
NeilClack 46a61a8397 Remove HDMI-A-1
Build Dev ISOs / build-dev-commercial (push) Successful in 4m17s
Build Dev ISOs / build-dev-android (push) Successful in 4m23s
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m56s
HDMI-A-1 refers to the HDMI port closest to the power cable.
In all models, we end up connecting HDMI to port 2.
HDMI-A-1 was likely taking over, becuase it suddenly had a valid
EDID assigned, indicating 'a monitor is connected here'
2026-08-25 22:26:00 -05:00
NeilClack c1ad4a9767 Remove archive steps...
Build Dev ISOs / build-dev-android (push) Successful in 4m41s
Build Dev ISOs / build-dev-kiosk (push) Successful in 4m34s
Build Dev ISOs / build-dev-commercial (push) Successful in 5m6s
2026-08-25 21:49:09 -05:00
NeilClack 47dc7b8964 Update dev builds to build each -dev variant
Build Dev ISOs / build-dev-kiosk (push) Failing after 3m18s
Build Dev ISOs / build-dev-android (push) Canceled after 0s
Build Dev ISOs / build-dev-commercial (push) Canceled after 2m20s
2026-08-25 21:43:20 -05:00
NeilClack dfa7320a37 Test 1 - EDID Spoofing for Android 4K
Build Dev ISO / build-dev-iso (push) Canceled after 2m40s
2026-08-25 21:40:56 -05:00
NeilClack 87fca854f4 Update radiantwave - rotated
Build Dev ISO / build-dev-iso (push) Successful in 4m13s
2026-08-25 11:31:56 -05:00
NeilClack c8298a128f Update Radiantwave 1.3
Build Dev ISO / build-dev-iso (push) Successful in 4m38s
Stretch scroller screen on vertical displays. Test 1
2026-08-25 11:00:28 -05:00
NeilClack 036c437430 Update RadiantWave 1.2
Build Dev ISO / build-dev-iso (push) Successful in 4m31s
2026-08-25 10:33:59 -05:00
NeilClack 8775fcfb24 Update workflows to run automatically on push to main
Build Dev ISO / build-dev-iso (push) Canceled after 0s
2026-08-25 08:09:26 -05:00
NeilClack f900585702 Update Radiantwave 1.2
Build Dev ISO / build-dev-iso (push) Successful in 4m51s
2026-08-25 08:01:13 -05:00
NeilClack 16b8e3aeb9 Update Radiantwave 1.1
Build Dev ISO / build-dev-iso (push) Failing after 3m37s
2026-08-25 06:59:10 -05:00
NeilClack 45b4d7aa95 Update radiantwave
Build Dev ISO / build-dev-iso (push) Successful in 4m21s
2026-08-23 21:20:38 -05:00
27 changed files with 656 additions and 374 deletions
-51
View File
@@ -1,51 +0,0 @@
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
@@ -1,43 +0,0 @@
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}"
@@ -1,43 +0,0 @@
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
@@ -1,43 +0,0 @@
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
@@ -1,42 +0,0 @@
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}"
+150
View File
@@ -0,0 +1,150 @@
# One build workflow for every branch — do not add per-branch copies.
#
# The branch/ref selects the channel, nothing else does: the flake's `channel`
# block picks the app input, Attic repo, and upgrade ref, so the same config
# names build dev images on `dev` and prod images on `main`. This file itself
# is identical on both branches.
#
# 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
#
# Hardware variants come from an include-matrix:
# ISO variants -> .config.system.build.isoImage (storeContents pulls in the closure)
# Raspberry Pi -> .config.system.build.sdImage
# `orion-lxc` emulates aarch64-linux, so the Pi build runs alongside the rest.
name: Build Kiosk Images
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.
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=
jobs:
build:
name: ${{ matrix.label }}
runs-on: orion-lxc
strategy:
fail-fast: false
matrix:
include:
- label: kiosk
system: kiosk-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: android
system: android-iso
target: isoImage
outdir: iso
ext: iso
arch: x86_64-linux
manifest: manifest.json
- 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)"
# Production only: fail fast unless the baked closure is exactly what
# RadiantWave CI published to Attic. The dev channel churns on every
# dispatch, so this is skipped there.
- name: Verify baked baseline matches the published channel
run: |
set -euo pipefail
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
if [ "$CHANNEL" != "radiantwave" ]; then
echo "channel '$CHANNEL' is not production; skipping baseline preflight"
exit 0
fi
VERSION=$(nix eval --impure --raw .#channelVersion)
ATTIC=$(nix eval --impure --raw .#channelAtticUrl)
MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}"
BASELINE=$(nix eval --impure --raw .#baselineOutPaths.${{ matrix.arch }})
MANIFEST=$(curl -fsSL "$MANIFEST_URL")
PUB_VERSION=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).version')
PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath')
echo "channel version : $VERSION"
echo "manifest version: $PUB_VERSION"
echo "baseline path : $BASELINE"
echo "manifest path : $PUB_PATH"
if [ "$VERSION" != "$PUB_VERSION" ]; then
echo "ERROR: channel.appVersion '$VERSION' != published manifest version '$PUB_VERSION'."
exit 1
fi
if [ "$BASELINE" != "$PUB_PATH" ]; then
echo "ERROR: baseline '$BASELINE' is not the published closure '$PUB_PATH'."
echo " Run 'nix flake update radiantwave' so the pinned input resolves to the released commit."
exit 1
fi
HASH=$(basename "$BASELINE" | cut -d- -f1)
if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then
echo "ERROR: $BASELINE 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}"
+22 -17
View File
@@ -8,10 +8,14 @@ Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig`
## Prod/dev split
Single branch (`main`) for this repo. The production vs. dev split is at the **flake input + upgrade ref** level:
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`:
- 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`.
- `main`: `channel.appInput = radiantwave` (pinned to tag `1.8`), `atticRepo = "radiantwave"`, `gitRef = ""` (default branch), `appVersion = "1.8"`.
- `dev`: `channel.appInput = radiantwave-dev`, `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`, `appVersion = <dev fingerprint>`.
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 four `channel` values. When merging, reconcile just this one block.
`channel.appVersion` is applied to the baked app via `appPackage = system: channel.appInput.packages.${system}.default.override { version = channel.appVersion; }`. RadiantWave's own flake only injects its version from the ambient `APP_VERSION` env var on impure evals, so pure evals (local `nix build`, `build-rpi.sh`) fall back to `0.1.0`. Declaring the version here keeps local and CI builds correct without `--impure`, and the derivation matches the Attic closure when RadiantWave CI built it with the same version string. Bump `appVersion` whenever the pinned input changes (release tag on `main`, short commit hash on `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.
@@ -36,16 +40,16 @@ 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 input |
|---|---|---|---|
| `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) |
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | `radiantwave` (tag 1.8) |
On the `dev` branch, the same four configs use `radiantwave-dev` and track `?ref=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.
@@ -56,11 +60,10 @@ 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 |
| `android-dev-iso` | `radiantwavekiosk-android-dev` | NVMe |
These same names build **dev-flavored** images when the flake is checked out on the `dev` branch (the channel block selects the dev app, attic repo, and upgrade ref), so there are no separate `-dev` ISOs.
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.
@@ -91,18 +94,20 @@ Regenerate when major hardware changes, not on a schedule — reports are snapsh
- **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` — `appInput`, `atticRepo`, `gitRef`, `appVersion`. 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`, 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.
- **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`.
- **Flake inputs**: `radiantwave` tracks main, `radiantwave-dev` tracks `?ref=dev`; prod and `-dev` configs consume them respectively. Updating them independently is intentional.
- **Flake inputs**: `radiantwave` is pinned to tag `1.8` (`?ref=refs/tags/1.8`); `radiantwave-dev` tracks `?ref=dev`. The `channel` block picks which one the configs consume. Bump the pinned tag and `channel.appVersion` together.
- **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, builders (`mkKioskSystem`, `mkRpiKioskSystem`, `mkIso`), all system definitions, facter report wiring
- `flake.nix` — entry point: inputs, the `channel` dev/prod selector, 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 six ISOs
- `zero-touch-installer.nix` — parameterized USB installer module used by all three 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
@@ -0,0 +1,19 @@
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.
+6 -3
View File
@@ -6,16 +6,19 @@ 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 '
@@ -23,8 +26,8 @@ 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 "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 "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 "max-jobs = 4" >> /etc/nix/nix.conf
echo "==> Building sdImage (first run slow, cached after)..."
@@ -34,7 +37,7 @@ podman run --rm \
--print-build-logs
echo "==> Build complete, copying..."
cp result/sd-image/*.img* /repo/'${FILENAME}'
cp result/sd-image/*.img.zst /repo/'${FILENAME}'
echo "==> Done: ${FILENAME}"
ls -lh /repo/'${FILENAME}'
'
+12 -32
View File
@@ -1,7 +1,15 @@
{ pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }:
{ config, pkgs, radiantwaveApp, upgradeFlakeRef, radiantwavePlymouth, ... }:
let
radiantApp = radiantwave.packages.${pkgs.system}.default;
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";
myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ];
in
{
@@ -27,7 +35,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 ];
boot.plymouth.themePackages = [ (radiantwavePlymouth pkgs) ];
# Silence the kernel/initrd output behind the splash. The module adds
# "splash" itself; we keep "quiet" plus show_status=false and loglevel 0.
@@ -159,7 +167,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 -- /run/current-system/sw/bin/radiantwave -d -s > /tmp/kiosk.log 2>&1
exec dbus-run-session cage -- ${launchBin} -d -s > /tmp/kiosk.log 2>&1
fi
'';
@@ -255,32 +263,4 @@ 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
@@ -0,0 +1,74 @@
{ 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
+10 -10
View File
@@ -220,16 +220,16 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1787341934,
"narHash": "sha256-pWGpgM5acdCO02H8P3KKtdv09n9ucnI3cg8d3/tvwtk=",
"ref": "main",
"rev": "b96631c374682088088b3d679359d192a0ae74d6",
"revCount": 35,
"lastModified": 1790803867,
"narHash": "sha256-HRGQQlmn/kCsZIq1wBMFaW6q1xfSvwPhUS5CpG1mm+Q=",
"ref": "refs/tags/1.8",
"rev": "34c72b51ddc3da8d46d994e787ffd1f8e8dda77c",
"revCount": 85,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
"original": {
"ref": "main",
"ref": "refs/tags/1.8",
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
}
@@ -240,11 +240,11 @@
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1787522765,
"narHash": "sha256-D3KOrGcAH07lvKYgyiwejOQpDW91js7zI6l87WdabLk=",
"lastModified": 1787943320,
"narHash": "sha256-zoHqtwO0DV3z/N311IxiMLrbyQHrw7HIoL1FQW5pGDk=",
"ref": "dev",
"rev": "63265cc22f3055a782a0114e92c57a36bef723b2",
"revCount": 40,
"rev": "2917d33866f052eca1e9daea175d95382a9087f2",
"revCount": 50,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
+126 -86
View File
@@ -4,7 +4,7 @@
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.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=refs/tags/1.8";
radiantwave-dev.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=dev";
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
};
@@ -28,33 +28,87 @@
# 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).
radiantwavePlymouth = pkgs.stdenv.mkDerivation {
pname = "radiantwave-plymouth";
version = "1.0.0";
mkRadiantwavePlymouth = targetPkgs:
targetPkgs.stdenv.mkDerivation {
pname = "radiantwave-plymouth";
version = "1.0.0";
nativeBuildInputs = [ pkgs.librsvg ];
nativeBuildInputs = [ targetPkgs.librsvg ];
src = ./assets/RWT-Circular-Color-ForOnDark.svg;
dontUnpack = true;
src = ./assets/RWT-Circular-Color-ForOnDark.svg;
dontUnpack = true;
buildPhase = ''
mkdir -p $out/share/plymouth/themes/radiantwave
rsvg-convert \
--keep-aspect-ratio \
--background-color=#111318 \
--width=1280 \
--height=720 \
--output=$out/share/plymouth/themes/radiantwave/splash.png \
$src
sed 's,/usr/,'"$out"'/,g' ${./plymouth-theme/radiantwave.plymouth} \
> $out/share/plymouth/themes/radiantwave/radiantwave.plymouth
cp ${./plymouth-theme/radiantwave.script} \
$out/share/plymouth/themes/radiantwave/radiantwave.script
'';
};
buildPhase = ''
mkdir -p $out/share/plymouth/themes/radiantwave
rsvg-convert \
--keep-aspect-ratio \
--background-color=#111318 \
--width=1280 \
--height=720 \
--output=$out/share/plymouth/themes/radiantwave/splash.png \
$src
sed 's,/usr/,'"$out"'/,g' ${./plymouth-theme/radiantwave.plymouth} \
> $out/share/plymouth/themes/radiantwave/radiantwave.plymouth
cp ${./plymouth-theme/radiantwave.script} \
$out/share/plymouth/themes/radiantwave/radiantwave.script
'';
};
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: appInput = radiantwave (tag 1.8), atticRepo = "radiantwave",
# gitRef = "", appVersion = "1.8"
# dev: appInput = radiantwave-dev, atticRepo = "radiantwave-dev",
# gitRef = "?ref=dev", appVersion = <dev fingerprint>
#
# To flip branches, change ONLY these four values (then push/merge).
channel = {
appInput = radiantwave-dev; # dev
atticRepo = "radiantwave-dev"; # dev
gitRef = "?ref=dev"; # dev
appVersion = "2917d33"; # dev
};
# Exposed so CI (see .gitea/workflows/build-prod-*.yml) and humans can
# verify which channel this checkout resolves to. On `main` this must be
# "radiantwave"; on `dev` it must be "radiantwave-dev". The prod
# workflows fail fast if this isn't the production channel, so a merge
# that brings a dev-pointing channel block into `main` breaks the build
# instead of silently shipping dev builds as prod ISOs.
channelName = if channel.appInput == radiantwave then "radiantwave" else "radiantwave-dev";
# The App package, pinned to the channel's declared version. RadiantWave's
# own flake only injects its version from the ambient APP_VERSION env var
# on impure evals; pure evals (local `nix build`, build-rpi.sh) fall back
# to its 0.1.0 default. Overriding the version here makes the baked
# baseline correct regardless of eval purity, and matches the Attic
# closure when RadiantWave CI built it with the same version string.
appPackage = system: channel.appInput.packages.${system}.default.override {
version = channel.appVersion;
};
# 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;
baselinePackage = baseline;
manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}";
atticUrl = "https://attic.radiantwavetech.com/${repo}";
};
};
mkKioskSystem = {
system,
radiantwave,
@@ -67,9 +121,9 @@
inherit system;
specialArgs = {
inherit radiantwave;
inherit upgradeFlakeRef;
inherit radiantwavePlymouth;
inherit radiantwave upgradeFlakeRef;
radiantwaveApp = appPackage system;
radiantwavePlymouth = mkRadiantwavePlymouth;
};
modules = [
@@ -79,6 +133,7 @@
system.configurationRevision = revision;
}
./configuration.nix
./radiantwave-updater.nix
disko.nixosModules.disko
diskModule
nixos-facter-modules.nixosModules.facter
@@ -103,10 +158,12 @@
specialArgs = {
inherit radiantwave upgradeFlakeRef;
inherit nixos-raspberrypi;
inherit radiantwavePlymouth;
radiantwaveApp = appPackage "aarch64-linux";
radiantwavePlymouth = mkRadiantwavePlymouth;
};
modules = [
{ nixpkgs.config.allowUnfree = true; }
({ ... }: {
imports = with nixos-raspberrypi.nixosModules; [
sd-image
@@ -116,6 +173,7 @@
];
})
./configuration.nix
./radiantwave-updater.nix
radiantwave.nixosModules.default
{
networking.hostName = hostName;
@@ -166,82 +224,76 @@
};
in
{
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;
inherit channelName;
# Kiosk systems.
packages.x86_64-linux.radiantwavekiosk = self.nixosConfigurations.radiantwavekiosk.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-commercial = self.nixosConfigurations.radiantwavekiosk-commercial.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-android = self.nixosConfigurations.radiantwavekiosk-android.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-rpi = self.nixosConfigurations.radiantwavekiosk-rpi.config.system.build.toplevel;
# Kiosk systems — one per hardware target, no dev/prod siblings.
#
# 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.
# Every config consumes the channel selected at the top of this file
# (see `channel`): the RadiantWave app input, 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.
nixosConfigurations = {
radiantwavekiosk = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
radiantwave = channel.appInput;
hostName = "radiantwavekiosk";
diskModule = ./disk-config.nix;
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";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk";
extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
];
};
radiantwavekiosk-commercial = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-commercial";
diskModule = ./commercial-disk-config.nix;
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";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial";
extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
];
};
# Rotated screen for android-style displays.
radiantwavekiosk-android = mkKioskSystem {
system = "x86_64-linux";
radiantwave = radiantwave;
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-android";
diskModule = ./disk-config.nix;
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];
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android";
extraModules = [
./rotated_display.nix
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
];
};
# Raspberry Pi 5.
radiantwavekiosk-rpi = mkRpiKioskSystem {
radiantwave = radiantwave;
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-rpi";
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";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi";
extraModules = [
(radiantwaveUpdaterModule {
repo = channel.atticRepo;
baseline = appPackage "aarch64-linux";
manifestFile = "aarch64-manifest.json";
})
];
};
# === 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
# app, attic repo, and upgrade ref), so there are no separate -dev
# ISOs.
kiosk-iso = mkIso {
targetConfig = "radiantwavekiosk";
diskModule = ./disk-config.nix;
@@ -254,18 +306,6 @@
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
@@ -0,0 +1,128 @@
#!/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
@@ -0,0 +1,98 @@
{ 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).";
};
baselinePackage = lib.mkOption {
type = lib.types.package;
description = "Copy of RadiantWave baked into the system closure, 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 baseline copy already baked into the system closure
# (so the ISO carries it with no duplication); the updater service later
# re-points it atomically to newer closures fetched from Attic.
systemd.tmpfiles.rules = [
"L /var/lib/radiantwave/current - - - - ${cfg.baselinePackage.outPath}"
];
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}"
'';
};
systemd.timers.radiantwave-updater = {
description = "Check for RadiantWave app updates";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "2min";
Persistent = true;
Unit = "radiantwave-updater.service";
};
};
};
}
+10 -3
View File
@@ -1,6 +1,13 @@
{ pkgs, radiantwave, ...}:
{ config, pkgs, radiantwaveApp, ...}:
let
radiantApp = radiantwave.packages.${pkgs.system}.default;
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";
kioskscript = pkgs.writeShellScript "kiosk-start" ''
# Wait for the Wayland display to be ready
for i in {1..10}; do
@@ -13,7 +20,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 ${radiantApp}/bin/radiantwave -d -s
exec ${launchBin} -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.4.0/24}"
SUBNET="${1:-192.168.1.0/24}"
SSH_USER="kiosk"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new)