From dfa7320a37d9ffa32724a001530512ac6b5feeba Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 25 Aug 2026 21:40:56 -0500 Subject: [PATCH 01/16] Test 1 - EDID Spoofing for Android 4K --- android-4k30-edid.bin | Bin 0 -> 256 bytes donor.bin | Bin 0 -> 256 bytes edid-override.nix | 74 +++++++++++++++++++++ flake.nix | 26 +++++++- force_4k30_portrait_only.py | 128 ++++++++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 android-4k30-edid.bin create mode 100644 donor.bin create mode 100644 edid-override.nix create mode 100644 force_4k30_portrait_only.py diff --git a/android-4k30-edid.bin b/android-4k30-edid.bin new file mode 100644 index 0000000000000000000000000000000000000000..5ea79139eb166e9e14dfaa39d0fde1c05d03d58e GIT binary patch literal 256 zcmZSh4+acDTO`#O85kH$l^B^D1UM_Wcuy_}cb}my4 z_yidkk14^KQKMWzIA*DW: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); + }; +} diff --git a/flake.nix b/flake.nix index 8d082ab..1acb204 100644 --- a/flake.nix +++ b/flake.nix @@ -216,7 +216,18 @@ hostName = "radiantwavekiosk-android"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android"; - extraModules = [./rotated_display.nix]; + extraModules = [ + ./rotated_display.nix + ./edid-override.nix + { services.kioskEdid = { + enable = true; + overrides = [ + { connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; } + { connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; } + ]; + }; + } + ]; }; radiantwavekiosk-android-dev = mkKioskSystem { system = "x86_64-linux"; @@ -224,7 +235,18 @@ 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]; + extraModules = [ + ./rotated_display.nix + ./edid-override.nix + { services.kioskEdid = { + enable = true; + overrides = [ + { connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; } + { connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; } + ]; + }; + } + ]; }; # Raspberry Pi 5. radiantwavekiosk-rpi = mkRpiKioskSystem { diff --git a/force_4k30_portrait_only.py b/force_4k30_portrait_only.py new file mode 100644 index 0000000..63c718a --- /dev/null +++ b/force_4k30_portrait_only.py @@ -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.") -- 2.43.0 From 47dc7b896449d9112373637caf6ed62dc5695659 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 25 Aug 2026 21:43:20 -0500 Subject: [PATCH 02/16] Update dev builds to build each -dev variant --- .gitea/workflows/build-dev.yml | 96 +++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/build-dev.yml b/.gitea/workflows/build-dev.yml index dd685ba..f22773e 100644 --- a/.gitea/workflows/build-dev.yml +++ b/.gitea/workflows/build-dev.yml @@ -1,4 +1,4 @@ -name: Build Dev ISO +name: Build Dev ISOs on: workflow_dispatch: @@ -12,9 +12,8 @@ env: extra-trusted-public-keys = radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= jobs: - build-dev-iso: + build-dev-kiosk: 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 . @@ -25,27 +24,98 @@ jobs: - name: Prepare output directory run: mkdir -p /home/user/radiantwave - # --- Kiosk ISO --- - - name: Build kiosk ISO + - name: Build dev 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 + - name: Rename dev kiosk ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) - FILENAME="radiantwave-kiosk-dev-iso-${ISO_DATE}.iso" + FILENAME="radiantwave-kiosk-dev-${ISO_DATE}.iso" cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" - echo "KIOSK_ISO_FILENAME=${FILENAME}" >> "$GITHUB_ENV" + echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - name: Move old dev images to archive + - name: Move old dev kiosk 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 + - name: Copy dev kiosk ISO to fileserver run: | - scp "/home/user/radiantwave/${KIOSK_ISO_FILENAME}" \ + scp "/home/user/radiantwave/${FILENAME}" \ radiantwave-infra:/mnt/infrastructure_data/files/images/ - - name: Cleanup kiosk ISO - run: rm -f "/home/user/radiantwave/${KIOSK_ISO_FILENAME}" + - name: Cleanup + run: rm -f "/home/user/radiantwave/${FILENAME}" + + build-dev-commercial: + 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 + + - name: Build dev commercial ISO + run: | + export NIXPKGS_ALLOW_UNFREE=1 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + nix build -L --impure .#nixosConfigurations.commercial-dev-iso.config.system.build.isoImage + + - name: Rename dev commercial ISO + run: | + ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) + FILENAME="radiantwave-commercial-dev-${ISO_DATE}.iso" + cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" + echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" + + - name: Move old dev commercial 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-commercial-dev*' + + - name: Copy dev commercial ISO to fileserver + run: | + scp "/home/user/radiantwave/${FILENAME}" \ + radiantwave-infra:/mnt/infrastructure_data/files/images/ + + - name: Cleanup + run: rm -f "/home/user/radiantwave/${FILENAME}" + + build-dev-android: + 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 + + - name: Build dev android ISO + run: | + export NIXPKGS_ALLOW_UNFREE=1 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage + + - name: Rename dev android ISO + run: | + ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) + FILENAME="radiantwave-android-dev-${ISO_DATE}.iso" + cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" + echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" + + - name: Move old dev android 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-android-dev*' + + - name: Copy dev android ISO to fileserver + run: | + scp "/home/user/radiantwave/${FILENAME}" \ + radiantwave-infra:/mnt/infrastructure_data/files/images/ + + - name: Cleanup + run: rm -f "/home/user/radiantwave/${FILENAME}" -- 2.43.0 From c1ad4a9767260673fcf3d5236c1805b4f9afb2a1 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 25 Aug 2026 21:49:09 -0500 Subject: [PATCH 03/16] Remove archive steps... --- .gitea/workflows/build-dev.yml | 75 ++++++++++------------ .gitea/workflows/build-prod-android.yml | 4 -- .gitea/workflows/build-prod-commercial.yml | 4 -- .gitea/workflows/build-prod-kiosk.yml | 4 -- .gitea/workflows/build-prod-rpi.yml | 3 - 5 files changed, 33 insertions(+), 57 deletions(-) diff --git a/.gitea/workflows/build-dev.yml b/.gitea/workflows/build-dev.yml index f22773e..138b91a 100644 --- a/.gitea/workflows/build-dev.yml +++ b/.gitea/workflows/build-dev.yml @@ -12,6 +12,39 @@ env: extra-trusted-public-keys = radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= jobs: + build-dev-android: + 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 + + - name: Build dev android ISO + run: | + export NIXPKGS_ALLOW_UNFREE=1 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage + + - name: Rename dev android ISO + run: | + ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) + FILENAME="radiantwave-android-dev-${ISO_DATE}.iso" + cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" + echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" + + - name: Copy dev android ISO to fileserver + run: | + scp "/home/user/radiantwave/${FILENAME}" \ + radiantwave-infra:/mnt/infrastructure_data/files/images/ + + - name: Cleanup + run: rm -f "/home/user/radiantwave/${FILENAME}" + build-dev-kiosk: runs-on: build-server steps: @@ -37,9 +70,6 @@ jobs: cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - name: Move old dev kiosk 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 dev kiosk ISO to fileserver run: | scp "/home/user/radiantwave/${FILENAME}" \ @@ -73,9 +103,6 @@ jobs: cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - name: Move old dev commercial 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-commercial-dev*' - - name: Copy dev commercial ISO to fileserver run: | scp "/home/user/radiantwave/${FILENAME}" \ @@ -83,39 +110,3 @@ jobs: - name: Cleanup run: rm -f "/home/user/radiantwave/${FILENAME}" - - build-dev-android: - 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 - - - name: Build dev android ISO - run: | - export NIXPKGS_ALLOW_UNFREE=1 - . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage - - - name: Rename dev android ISO - run: | - ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) - FILENAME="radiantwave-android-dev-${ISO_DATE}.iso" - cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" - echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - - name: Move old dev android 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-android-dev*' - - - name: Copy dev android ISO to fileserver - run: | - scp "/home/user/radiantwave/${FILENAME}" \ - radiantwave-infra:/mnt/infrastructure_data/files/images/ - - - name: Cleanup - run: rm -f "/home/user/radiantwave/${FILENAME}" diff --git a/.gitea/workflows/build-prod-android.yml b/.gitea/workflows/build-prod-android.yml index 7b4912f..3770704 100644 --- a/.gitea/workflows/build-prod-android.yml +++ b/.gitea/workflows/build-prod-android.yml @@ -33,10 +33,6 @@ jobs: 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}" \ diff --git a/.gitea/workflows/build-prod-commercial.yml b/.gitea/workflows/build-prod-commercial.yml index d130db4..59134d1 100644 --- a/.gitea/workflows/build-prod-commercial.yml +++ b/.gitea/workflows/build-prod-commercial.yml @@ -33,10 +33,6 @@ jobs: 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}" \ diff --git a/.gitea/workflows/build-prod-kiosk.yml b/.gitea/workflows/build-prod-kiosk.yml index 8a5d69d..e5e89ef 100644 --- a/.gitea/workflows/build-prod-kiosk.yml +++ b/.gitea/workflows/build-prod-kiosk.yml @@ -33,10 +33,6 @@ jobs: 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}" \ diff --git a/.gitea/workflows/build-prod-rpi.yml b/.gitea/workflows/build-prod-rpi.yml index bb267ce..609ac5e 100644 --- a/.gitea/workflows/build-prod-rpi.yml +++ b/.gitea/workflows/build-prod-rpi.yml @@ -30,9 +30,6 @@ jobs: 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}" \ -- 2.43.0 From 46a61a83978953cdb22b9dd55e927f670564c88e Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 25 Aug 2026 22:26:00 -0500 Subject: [PATCH 04/16] Remove HDMI-A-1 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' --- flake.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/flake.nix b/flake.nix index 1acb204..62bf5a6 100644 --- a/flake.nix +++ b/flake.nix @@ -241,7 +241,6 @@ { services.kioskEdid = { enable = true; overrides = [ - { connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; } { connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; } ]; }; -- 2.43.0 From 2bf762acac4cf810e51c5f9712cb453224699d9d Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 26 Aug 2026 10:12:45 -0500 Subject: [PATCH 05/16] Add actual EDID from Android Kiosk --- android-4k30-edid.bin | Bin 256 -> 0 bytes android-kiosk-edid.bin | Bin 0 -> 256 bytes donor.bin | Bin 256 -> 0 bytes flake.nix | 5 ++--- 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 android-4k30-edid.bin create mode 100644 android-kiosk-edid.bin delete mode 100644 donor.bin diff --git a/android-4k30-edid.bin b/android-4k30-edid.bin deleted file mode 100644 index 5ea79139eb166e9e14dfaa39d0fde1c05d03d58e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmZSh4+acDTO`#O85kH$l^B^D1UM_Wcuy_}cb}my$b%?ylC!102? z;8Q_V!-j}1hMHx`3=DEWt^XMejf_o9&CD$<4Y(8(fFgewBAh&cOeg?~{ACaim04iJ z02O4+W@5Ja7?hY09~UbiucWBJB5Y@CBgo9C%*oE)40KNxGY^9TgM|&ywDcRXM;I#_ ym>)6xf6U6v_=ukw(-lls4HAVGx(*RKu26TSDF$o^WRNy+C@@2~6zC8rm4 z_yidkk14^KQKMWzIA*DW Date: Wed, 26 Aug 2026 11:38:17 -0500 Subject: [PATCH 06/16] Disable EDID Override --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 7186a8b..f7fa2af 100644 --- a/flake.nix +++ b/flake.nix @@ -220,7 +220,7 @@ ./rotated_display.nix ./edid-override.nix { services.kioskEdid = { - enable = true; + enable = false; overrides = [ { connector = "HDMI-A-2"; edidFile = ./android-kiosk-edid.bin; } ]; @@ -238,7 +238,7 @@ ./rotated_display.nix ./edid-override.nix { services.kioskEdid = { - enable = true; + enable = false; overrides = [ { connector = "HDMI-A-2"; edidFile = ./android-kiosk-edid.bin; } ]; -- 2.43.0 From b8d0f59c0a4e09f6d95772e8e4d037d48d19807b Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 26 Aug 2026 12:30:36 -0500 Subject: [PATCH 07/16] Remove EDID overwrite --- flake.nix | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/flake.nix b/flake.nix index f7fa2af..bdcf415 100644 --- a/flake.nix +++ b/flake.nix @@ -218,14 +218,6 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android"; extraModules = [ ./rotated_display.nix - ./edid-override.nix - { services.kioskEdid = { - enable = false; - overrides = [ - { connector = "HDMI-A-2"; edidFile = ./android-kiosk-edid.bin; } - ]; - }; - } ]; }; radiantwavekiosk-android-dev = mkKioskSystem { @@ -236,14 +228,6 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev"; extraModules = [ ./rotated_display.nix - ./edid-override.nix - { services.kioskEdid = { - enable = false; - overrides = [ - { connector = "HDMI-A-2"; edidFile = ./android-kiosk-edid.bin; } - ]; - }; - } ]; }; # Raspberry Pi 5. -- 2.43.0 From d6611626760a7239e00e5fa5903ccf58d28dc514 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 27 Aug 2026 20:58:07 -0500 Subject: [PATCH 08/16] Add live RadiantWave updater (dev variants) 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). --- configuration.nix | 12 +++++- flake.nix | 24 ++++++++++++ radiantwave-updater.nix | 85 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 radiantwave-updater.nix diff --git a/configuration.nix b/configuration.nix index e51b742..c0773aa 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,7 +1,15 @@ -{ pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }: +{ config, pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }: let radiantApp = radiantwave.packages.${pkgs.system}.default; + # When the live-updater module is enabled (currently dev variants only), + # 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 { @@ -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 ''; diff --git a/flake.nix b/flake.nix index bdcf415..4375868 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,18 @@ revision = self.rev or self.dirtyRev or null; + # Live app-update side channel, dev-only for now. The manifest is + # served by the fileserver; closures are fetched (never built) from the + # dev Attic cache. Promotes to prod once validated. + radiantwaveDevUpdaterModule = { baseline }: { + services.radiantwaveUpdater = { + enable = true; + manifestUrl = "https://files.radiantwavetech.com/radiantwave-dev/manifest.json"; + atticUrl = "https://attic.radiantwavetech.com/radiantwave-dev"; + baselinePackage = baseline; + }; + }; + mkKioskSystem = { system, radiantwave, @@ -79,6 +91,7 @@ system.configurationRevision = revision; } ./configuration.nix + ./radiantwave-updater.nix disko.nixosModules.disko diskModule nixos-facter-modules.nixosModules.facter @@ -116,6 +129,7 @@ ]; }) ./configuration.nix + ./radiantwave-updater.nix radiantwave.nixosModules.default { networking.hostName = hostName; @@ -194,6 +208,9 @@ hostName = "radiantwavekiosk-dev"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + ]; }; radiantwavekiosk-commercial = mkKioskSystem { system = "x86_64-linux"; @@ -208,6 +225,9 @@ hostName = "radiantwavekiosk-commercial-dev"; diskModule = ./commercial-disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-commercial-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + ]; }; # Rotated screen for android-style displays. radiantwavekiosk-android = mkKioskSystem { @@ -228,6 +248,7 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev"; extraModules = [ ./rotated_display.nix + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) ]; }; # Raspberry Pi 5. @@ -240,6 +261,9 @@ radiantwave = radiantwave-dev; hostName = "radiantwavekiosk-rpi-dev"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-rpi-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.aarch64-linux.default; }) + ]; }; # === USB Images === diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix new file mode 100644 index 0000000..af27ef7 --- /dev/null +++ b/radiantwave-updater.nix @@ -0,0 +1,85 @@ +{ 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"; + RemainAfterExit = true; + }; + 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 + echo "radiantwave-updater: already current (''${CURRENT})" + exit 0 + fi + + # Fetch the closure from the signed Attic cache only. This never + # builds from source; it fails outright if the path isn't there. + echo "radiantwave-updater: acquiring ''${NEW_PATH}" + ${pkgs.nix}/bin/nix copy --from "${cfg.atticUrl}" "$NEW_PATH" || { + 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"; + }; + }; + }; +} -- 2.43.0 From 48c31d0f0c0b4110ffafc1163173aca19c23be67 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 27 Aug 2026 21:26:45 -0500 Subject: [PATCH 09/16] Move home systems to first build slot --- .gitea/workflows/build-dev.yml | 73 +++++++----------- UPDATES.md | 19 +++++ temp/computer-direct_to_kiosk.bin | Bin 0 -> 256 bytes temp/root/computer-direct_to_kiosk.bin | Bin 0 -> 256 bytes ..._hdmi_to_kiosk-out2_wireless__unstable.bin | 0 ..._wireless-out2_hdmi_to_kiosk__unstable.bin | Bin 0 -> 256 bytes ...omputer-splitter_out_1-direct_to_kiosk.bin | Bin 0 -> 256 bytes ...computer-splitter_out_1-wireless-kiosk.bin | Bin 0 -> 256 bytes ...omputer-splitter_out_2-direct_to_kiosk.bin | Bin 0 -> 256 bytes ...computer-splitter_out_2-wireless-kiosk.bin | Bin 0 -> 256 bytes temp/root/computer-wireless.bin | 0 11 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 UPDATES.md create mode 100644 temp/computer-direct_to_kiosk.bin create mode 100644 temp/root/computer-direct_to_kiosk.bin create mode 100644 temp/root/computer-splitter-out1_hdmi_to_kiosk-out2_wireless__unstable.bin create mode 100644 temp/root/computer-splitter-out1_wireless-out2_hdmi_to_kiosk__unstable.bin create mode 100644 temp/root/computer-splitter_out_1-direct_to_kiosk.bin create mode 100644 temp/root/computer-splitter_out_1-wireless-kiosk.bin create mode 100644 temp/root/computer-splitter_out_2-direct_to_kiosk.bin create mode 100644 temp/root/computer-splitter_out_2-wireless-kiosk.bin create mode 100644 temp/root/computer-wireless.bin diff --git a/.gitea/workflows/build-dev.yml b/.gitea/workflows/build-dev.yml index 138b91a..665f111 100644 --- a/.gitea/workflows/build-dev.yml +++ b/.gitea/workflows/build-dev.yml @@ -1,80 +1,65 @@ name: Build Dev ISOs - 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-android: - 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 - - - name: Build dev android ISO - run: | - export NIXPKGS_ALLOW_UNFREE=1 - . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage - - - name: Rename dev android ISO - run: | - ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) - FILENAME="radiantwave-android-dev-${ISO_DATE}.iso" - cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" - echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - - name: Copy dev android ISO to fileserver - run: | - scp "/home/user/radiantwave/${FILENAME}" \ - radiantwave-infra:/mnt/infrastructure_data/files/images/ - - - name: Cleanup - run: rm -f "/home/user/radiantwave/${FILENAME}" - build-dev-kiosk: 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 - - name: Build dev 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 dev kiosk ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) FILENAME="radiantwave-kiosk-dev-${ISO_DATE}.iso" cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - name: Copy dev kiosk ISO to fileserver run: | scp "/home/user/radiantwave/${FILENAME}" \ radiantwave-infra:/mnt/infrastructure_data/files/images/ + - name: Cleanup + run: rm -f "/home/user/radiantwave/${FILENAME}" + build-dev-android: + 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 + - name: Build dev android ISO + run: | + export NIXPKGS_ALLOW_UNFREE=1 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage + - name: Rename dev android ISO + run: | + ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) + FILENAME="radiantwave-android-dev-${ISO_DATE}.iso" + cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" + echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" + - name: Copy dev android ISO to fileserver + run: | + scp "/home/user/radiantwave/${FILENAME}" \ + radiantwave-infra:/mnt/infrastructure_data/files/images/ - name: Cleanup run: rm -f "/home/user/radiantwave/${FILENAME}" @@ -83,30 +68,24 @@ jobs: 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 - - name: Build dev commercial ISO run: | export NIXPKGS_ALLOW_UNFREE=1 . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh nix build -L --impure .#nixosConfigurations.commercial-dev-iso.config.system.build.isoImage - - name: Rename dev commercial ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) FILENAME="radiantwave-commercial-dev-${ISO_DATE}.iso" cp -L result/iso/*.iso "/home/user/radiantwave/${FILENAME}" echo "FILENAME=${FILENAME}" >> "$GITHUB_ENV" - - name: Copy dev commercial ISO to fileserver run: | scp "/home/user/radiantwave/${FILENAME}" \ radiantwave-infra:/mnt/infrastructure_data/files/images/ - - name: Cleanup run: rm -f "/home/user/radiantwave/${FILENAME}" diff --git a/UPDATES.md b/UPDATES.md new file mode 100644 index 0000000..4dae974 --- /dev/null +++ b/UPDATES.md @@ -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 radiantwave-infra:/mnt/infrastructure_data/files/radiantwave-dev/manifest.json. main.yml untouched. +KioskConfig: +- New radiantwave-updater.nix module: tmpfiles seed (L /var/lib/radiantwave/current → ), 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? \ No newline at end of file diff --git a/temp/computer-direct_to_kiosk.bin b/temp/computer-direct_to_kiosk.bin new file mode 100644 index 0000000000000000000000000000000000000000..3c37dfb7921cb57001f3ee7f65d9c1b71f79700c GIT binary patch literal 256 zcmZSh4+abg9*hi(3=9k+vW(0P%VI0Ids&x<_|8}1^9)()!O(c1v0>$b%?ylC!102? z;8Q_V!-j}1hMHx`3=DEWt^XMejf_o9&CD$<4Y(8(fFgewBAh&cOeg?~{ACaim04iJ z02O4+W@5Ja7?hY09~UbiucWBJB5Y@CBgo9C%*oE)40KNxGY^9TgM|&ywDcRXM;I#_ ym>)6xf6U6v_=ukw(-lls4HAVGx(*RKu26TSDF$o^WRNy+C@@2~6zC8rm$b%?ylC!102? z;8Q_V!-j}1hMHx`3=DEWt^XMejf_o9&CD$<4Y(8(fFgewBAh&cOeg?~{ACaim04iJ z02O4+W@5Ja7?hY09~UbiucWBJB5Y@CBgo9C%*oE)40KNxGY^9TgM|&ywDcRXM;I#_ ym>)6xf6U6v_=ukw(-lls4HAVGx(*RKu26TSDF$o^WRNy+C@@2~6zC8rmI0LAi!C{#d~r=xcdxsArBv*I3pThvTBehw9s{k&~asWz$eJS zAjc@nP!xDUP9aYtl;O}?C!h$#JT&qTgNKW+r$TT+PG(6-Y7wI^6SHK2vw#wdGABEG XGa~~-Dl-p*0K+4OS7;h(#C;6_6a^mv literal 0 HcmV?d00001 diff --git a/temp/root/computer-splitter_out_1-direct_to_kiosk.bin b/temp/root/computer-splitter_out_1-direct_to_kiosk.bin new file mode 100644 index 0000000000000000000000000000000000000000..a9d79a9290b7fe35b121a03b144a1b5931656b53 GIT binary patch literal 256 zcmZSh4+abg9t>b0BFo6!uq?KMyO(u&i0^zAKF^S)9t@2K8XHy~*v!BP1uP{D2A>L| z8a709G1M$eW?+zm=td*|GKh!DERbX1QUC*>>>q{*Cl4qWD8?Ac#BBUAAU-ZuKwe2v zfkoKP)<%$-QJIsSy_u1LA&Z%ZL4m=-MuCCh5ySt-tjvs$`4ST_EoZW7kSMg!b%@Y$ Xg*qoqFLV0_5LELq?zpv0og$b0BFo6!uq?KMyO(u&i0^zAKF^S)9t@2K8XHy~*v!BP1uP{D2A>L| z8a709G1M$eW?+zm=td*|GKh!DERbX1QUC*>>>q{*Cl4qWD8?Ac#BBUAAU-ZuKwe2v zfkoKP)<%$-QJIsSy_u1LA&Z%ZL4m=-MuCCh5ySt-tjvs$`4ST_EoZW7kSMg!b%@Y$ Xg*qoqFLV0_5LELq?zpv0og$ Date: Fri, 28 Aug 2026 13:55:59 -0500 Subject: [PATCH 10/16] Update RadiantWave a9087f2 --- flake.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 8532e96..8cbaeca 100644 --- a/flake.lock +++ b/flake.lock @@ -240,11 +240,11 @@ "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1787673353, - "narHash": "sha256-aLWuba8+AVfYQc3GNBSb74d/p7F28K8yI2MUMZKq89k=", + "lastModified": 1787943320, + "narHash": "sha256-zoHqtwO0DV3z/N311IxiMLrbyQHrw7HIoL1FQW5pGDk=", "ref": "dev", - "rev": "400c6a782be0cda69fc41b47783693536e7265a3", - "revCount": 45, + "rev": "2917d33866f052eca1e9daea175d95382a9087f2", + "revCount": 50, "type": "git", "url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git" }, -- 2.43.0 From 7d8cc56ba03751198def31065d89af17fd7e5dad Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 15:25:04 -0500 Subject: [PATCH 11/16] Fetch updater closures from cache.nixos.org as well as Attic nix copy --from 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. --- radiantwave-updater.nix | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix index af27ef7..008de79 100644 --- a/radiantwave-updater.nix +++ b/radiantwave-updater.nix @@ -56,10 +56,15 @@ in exit 0 fi - # Fetch the closure from the signed Attic cache only. This never - # builds from source; it fails outright if the path isn't there. + # Realize the closure via the machine's substituters. The app itself + # only exists in the dev Attic cache, but nixpkgs deps (e.g. sqlite) + # are signed by cache.nixos.org and Attic intentionally skips them to + # avoid re-uploading the world. Fetch those from cache.nixos.org so + # the whole closure resolves without ever building from source. echo "radiantwave-updater: acquiring ''${NEW_PATH}" - ${pkgs.nix}/bin/nix copy --from "${cfg.atticUrl}" "$NEW_PATH" || { + ${pkgs.nix}/bin/nix-store -r "$NEW_PATH" \ + --option extra-substituters "https://attic.radiantwavetech.com/radiantwave-dev https://cache.nixos.org" \ + --option extra-trusted-public-keys "radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" || { echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}" exit 1 } -- 2.43.0 From 9a8a258228b88c25414ed2cbafd9360b83aee710 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 15:30:20 -0500 Subject: [PATCH 12/16] GC-root the fetched radiantwave closure 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. --- radiantwave-updater.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix index 008de79..968028b 100644 --- a/radiantwave-updater.nix +++ b/radiantwave-updater.nix @@ -52,6 +52,8 @@ in 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 @@ -61,8 +63,11 @@ in # are signed by cache.nixos.org and Attic intentionally skips them to # avoid re-uploading the world. Fetch those from cache.nixos.org so # the whole closure resolves without ever building from source. + # --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 "https://attic.radiantwavetech.com/radiantwave-dev https://cache.nixos.org" \ --option extra-trusted-public-keys "radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" || { echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}" -- 2.43.0 From ca89c234ab1505031d283f1ae6c3813d2d4a4ee1 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 15:59:23 -0500 Subject: [PATCH 13/16] Fix updater timer not re-arming after first run 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. --- radiantwave-updater.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix index 968028b..a298032 100644 --- a/radiantwave-updater.nix +++ b/radiantwave-updater.nix @@ -38,7 +38,10 @@ in wants = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; - RemainAfterExit = true; + # 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 -- 2.43.0 From 05bb98b1078e783bfa8c5ec835fd03c9b878a720 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 16:37:13 -0500 Subject: [PATCH 14/16] Enable live app updater on production variants 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. --- configuration.nix | 32 ++------------------------------ flake.nix | 37 +++++++++++++++++++++++++------------ radiantwave-updater.nix | 18 +++++++++--------- tools/find_systems.zsh | 2 +- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/configuration.nix b/configuration.nix index c0773aa..6838a40 100644 --- a/configuration.nix +++ b/configuration.nix @@ -2,8 +2,8 @@ let radiantApp = radiantwave.packages.${pkgs.system}.default; - # When the live-updater module is enabled (currently dev variants only), - # RadiantWave launches through the mutable /var/lib/radiantwave/current + # 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 = @@ -263,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)" - # ''; - # }; } diff --git a/flake.nix b/flake.nix index 4375868..5a191cd 100644 --- a/flake.nix +++ b/flake.nix @@ -55,15 +55,17 @@ revision = self.rev or self.dirtyRev or null; - # Live app-update side channel, dev-only for now. The manifest is - # served by the fileserver; closures are fetched (never built) from the - # dev Attic cache. Promotes to prod once validated. - radiantwaveDevUpdaterModule = { baseline }: { + # Live app-update side channel. The manifest is served by the + # fileserver; closures are fetched (never built) from the variant's + # Attic cache. `repo` selects the channel — "radiantwave" (prod, + # ?ref=main) or "radiantwave-dev" (dev, ?ref=dev). All variants follow + # this same update procedure; only the channel differs. + radiantwaveUpdaterModule = { repo, baseline }: { services.radiantwaveUpdater = { enable = true; - manifestUrl = "https://files.radiantwavetech.com/radiantwave-dev/manifest.json"; - atticUrl = "https://attic.radiantwavetech.com/radiantwave-dev"; baselinePackage = baseline; + manifestUrl = "https://files.radiantwavetech.com/${repo}/manifest.json"; + atticUrl = "https://attic.radiantwavetech.com/${repo}"; }; }; @@ -192,8 +194,9 @@ # Kiosk systems. # # 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. + # `-dev` variants consume `radiantwave-dev` (?ref=dev). All variants + # run the same live-app updater against their channel's Attic repo and + # manifest, and track this repo's matching branch via autoUpgrade. nixosConfigurations = { radiantwavekiosk = mkKioskSystem { system = "x86_64-linux"; @@ -201,6 +204,9 @@ hostName = "radiantwavekiosk"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk"; + extraModules = [ + (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) + ]; }; radiantwavekiosk-dev = mkKioskSystem { system = "x86_64-linux"; @@ -209,7 +215,7 @@ diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-dev"; extraModules = [ - (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) ]; }; radiantwavekiosk-commercial = mkKioskSystem { @@ -218,6 +224,9 @@ hostName = "radiantwavekiosk-commercial"; diskModule = ./commercial-disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-commercial"; + extraModules = [ + (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) + ]; }; radiantwavekiosk-commercial-dev = mkKioskSystem { system = "x86_64-linux"; @@ -226,7 +235,7 @@ diskModule = ./commercial-disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-commercial-dev"; extraModules = [ - (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) ]; }; # Rotated screen for android-style displays. @@ -238,6 +247,7 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android"; extraModules = [ ./rotated_display.nix + (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) ]; }; radiantwavekiosk-android-dev = mkKioskSystem { @@ -248,7 +258,7 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev"; extraModules = [ ./rotated_display.nix - (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) ]; }; # Raspberry Pi 5. @@ -256,13 +266,16 @@ radiantwave = radiantwave; hostName = "radiantwavekiosk-rpi"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-rpi"; + extraModules = [ + (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.aarch64-linux.default; }) + ]; }; 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"; extraModules = [ - (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.aarch64-linux.default; }) + (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.aarch64-linux.default; }) ]; }; diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix index a298032..a020efb 100644 --- a/radiantwave-updater.nix +++ b/radiantwave-updater.nix @@ -61,18 +61,18 @@ in exit 0 fi - # Realize the closure via the machine's substituters. The app itself - # only exists in the dev Attic cache, but nixpkgs deps (e.g. sqlite) - # are signed by cache.nixos.org and Attic intentionally skips them to - # avoid re-uploading the world. Fetch those from cache.nixos.org so - # the whole closure resolves without ever building from source. - # --add-root pins the fetched closure so GC can't collect it while - # the swap below makes it live. + # 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 "https://attic.radiantwavetech.com/radiantwave-dev https://cache.nixos.org" \ - --option extra-trusted-public-keys "radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" || { + --option extra-substituters "${cfg.atticUrl} https://cache.nixos.org" || { echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}" exit 1 } diff --git a/tools/find_systems.zsh b/tools/find_systems.zsh index bc8fc9e..7e47578 100755 --- a/tools/find_systems.zsh +++ b/tools/find_systems.zsh @@ -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) -- 2.43.0 From b136d1751b583f628c443216e70c82991cb10bb6 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 20:15:32 -0500 Subject: [PATCH 15/16] Merge -dev closures. 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. --- .gitea/workflows/build-dev.yml | 6 +- AGENTS.md | 28 ++++---- flake.nix | 121 ++++++++++++--------------------- 3 files changed, 63 insertions(+), 92 deletions(-) diff --git a/.gitea/workflows/build-dev.yml b/.gitea/workflows/build-dev.yml index 665f111..6b8bc7d 100644 --- a/.gitea/workflows/build-dev.yml +++ b/.gitea/workflows/build-dev.yml @@ -22,7 +22,7 @@ jobs: 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 + nix build -L --impure .#nixosConfigurations.kiosk-iso.config.system.build.isoImage - name: Rename dev kiosk ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) @@ -49,7 +49,7 @@ jobs: run: | export NIXPKGS_ALLOW_UNFREE=1 . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - nix build -L --impure .#nixosConfigurations.android-dev-iso.config.system.build.isoImage + nix build -L --impure .#nixosConfigurations.android-iso.config.system.build.isoImage - name: Rename dev android ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) @@ -76,7 +76,7 @@ jobs: run: | export NIXPKGS_ALLOW_UNFREE=1 . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - nix build -L --impure .#nixosConfigurations.commercial-dev-iso.config.system.build.isoImage + nix build -L --impure .#nixosConfigurations.commercial-iso.config.system.build.isoImage - name: Rename dev commercial ISO run: | ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S) diff --git a/AGENTS.md b/AGENTS.md index b4b741f..589965f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,10 +8,12 @@ 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#` on `main`. -- `-dev` variants consume the `radiantwave-dev` input (`?ref=dev`) and auto-upgrade from `KioskConfig.git?ref=dev#-dev`. +- `main`: `channel.appInput = radiantwave` (main), `atticRepo = "radiantwave"`, `gitRef = ""` (default branch). +- `dev`: `channel.appInput = radiantwave-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 three `channel` values. When merging, reconcile just this one block. 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 +38,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) | + +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 +58,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 +92,19 @@ 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`. 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`. - **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` tracks main, `radiantwave-dev` tracks `?ref=dev`. The `channel` block picks which one the configs consume. 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, 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 diff --git a/flake.nix b/flake.nix index 5a191cd..ab07723 100644 --- a/flake.nix +++ b/flake.nix @@ -55,11 +55,29 @@ 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 (main), atticRepo = "radiantwave", + # gitRef = "" (default branch) + # dev: appInput = radiantwave-dev, atticRepo = "radiantwave-dev", + # gitRef = "?ref=dev" + # + # To flip branches, change ONLY these three values (then push/merge). + channel = { + appInput = radiantwave; # main + atticRepo = "radiantwave"; # main + gitRef = ""; # main + }; + # Live app-update side channel. The manifest is served by the - # fileserver; closures are fetched (never built) from the variant's - # Attic cache. `repo` selects the channel — "radiantwave" (prod, - # ?ref=main) or "radiantwave-dev" (dev, ?ref=dev). All variants follow - # this same update procedure; only the channel differs. + # fileserver; closures are fetched (never built) from the channel's + # Attic cache. `repo` selects the channel — "radiantwave" (main) or + # "radiantwave-dev" (dev). All variants follow this same update + # procedure; only the channel differs. radiantwaveUpdaterModule = { repo, baseline }: { services.radiantwaveUpdater = { enable = true; @@ -183,99 +201,58 @@ 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; - # Kiosk systems. + # 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). All variants - # run the same live-app updater against their channel's Attic repo and - # manifest, and track this repo's matching branch via autoUpgrade. + # 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"; + upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk"; extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) - ]; - }; - 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"; - extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = channel.appInput.packages.x86_64-linux.default; }) ]; }; 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"; + upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial"; extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) - ]; - }; - 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"; - extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = channel.appInput.packages.x86_64-linux.default; }) ]; }; # 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"; + upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android"; extraModules = [ ./rotated_display.nix - (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.x86_64-linux.default; }) - ]; - }; - 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 - (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.x86_64-linux.default; }) + (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = channel.appInput.packages.x86_64-linux.default; }) ]; }; # 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"; + upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi"; extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave"; baseline = radiantwave.packages.aarch64-linux.default; }) - ]; - }; - 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"; - extraModules = [ - (radiantwaveUpdaterModule { repo = "radiantwave-dev"; baseline = radiantwave-dev.packages.aarch64-linux.default; }) + (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = channel.appInput.packages.aarch64-linux.default; }) ]; }; @@ -283,6 +260,10 @@ # 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; @@ -295,18 +276,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; - }; }; }; } -- 2.43.0 From c18e158b699b443a61ba28663f73e79ac8c8a25f Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 20:30:51 -0500 Subject: [PATCH 16/16] Point dev branch at the dev RadiantWave channel Fixes b136d17 which left the channel block on main values, so the dev branch was building/tracking prod instead of radiantwave-dev. --- flake.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index ab07723..cbddecc 100644 --- a/flake.nix +++ b/flake.nix @@ -68,9 +68,9 @@ # # To flip branches, change ONLY these three values (then push/merge). channel = { - appInput = radiantwave; # main - atticRepo = "radiantwave"; # main - gitRef = ""; # main + appInput = radiantwave-dev; # dev + atticRepo = "radiantwave-dev"; # dev + gitRef = "?ref=dev"; # dev }; # Live app-update side channel. The manifest is served by the -- 2.43.0