From 704e126adc9bdaaa50106aee32426833f7afdfd9 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 26 Apr 2026 00:47:59 +0200 Subject: [PATCH] Landing: telemetry collector, doomguard watermark, SIGNAL centering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - telemetry.sh: bash collector for static device + OBS profile specs. Default mode wraps telemetry.json → telemetry.js (script-tag-loaded global, dodging file:// fetch CORS in CEF). --collect re-reads hardware + OBS basic.ini / streamEncoder.json. - index.html: footer subtitle now rotates real CPU/MEM/GPU/OBS data with soft 400ms crossfade; SIGNAL group re-centered via 3-col grid; doomguard watermark behind everything with periodic "tune-in" spikes through the noise canvas. - telemetry.json: hand-edited cosmetic version of collected specs. - .gitignore: track telemetry.json (source), ignore telemetry.js (build artifact) and *.manual backups. --- .gitignore | 11 ++ landing/index.html | 165 +++++++++++++++++++++++++++- landing/telemetry.json | 41 +++++++ landing/telemetry.sh | 237 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 landing/telemetry.json create mode 100755 landing/telemetry.sh diff --git a/.gitignore b/.gitignore index ceeda0a..a2954df 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,17 @@ plugin_config/obs-websocket/ *.env .env* +# ============================================================ +# Machine-local generated data +# ============================================================ + +# Generated wrapper for telemetry.json — built by telemetry.sh, not edited. +# (The JSON itself is tracked: it's the hand-edited source for the wrapper.) +landing/telemetry.js + +# Local backups created during edits — keep out of git noise. +landing/*.manual + # ============================================================ # OBS runtime / generated state — no value in version control # ============================================================ diff --git a/landing/index.html b/landing/index.html index 0a4131d..7ca3182 100644 --- a/landing/index.html +++ b/landing/index.html @@ -36,8 +36,8 @@ body { /* ───────── HUD top strip ───────── */ .hud { - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: 1fr 1fr 1fr; align-items: center; font-family: var(--mono); font-size: 22px; @@ -46,6 +46,9 @@ body { letter-spacing: 0.08em; } .hud .group { display: flex; gap: 28px; align-items: center; } +.hud > .group:nth-child(1) { justify-self: start; } +.hud > .group:nth-child(2) { justify-self: center; } +.hud > .group:nth-child(3) { justify-self: end; } .hud .dim { color: var(--hud-dim); } .hud .led { display: inline-block; @@ -99,7 +102,7 @@ body { text-align: center; } .standby .big { - font-size: 172px; + font-size: 130px; font-weight: 900; letter-spacing: 0.20em; line-height: 1; @@ -129,6 +132,68 @@ body { letter-spacing: 0.12em; } .foot .warn { color: var(--warn); } +#rig { transition: opacity 0.4s ease; } + +/* ───────── Doomguard watermark ───────── + Sits at z-index: -1 — below all flow content (HUD, standby, etc.) + and below the static canvas, so the white noise paints on top of it. + Mostly buried under noise (~4% opacity, blurred); periodic "tune-in" + spikes raise opacity + sharpen blur briefly, like a CRT catching signal. */ +.watermark { + position: absolute; + top: 50%; + left: 50%; + width: 38vw; + height: auto; + transform: translate(-50%, -50%); + pointer-events: none; + user-select: none; + z-index: -1; + opacity: 0; + filter: blur(2px) brightness(0.9); + animation: logo-tune-in 27s linear infinite; +} + +/* Logo is invisible by default. Three irregular "almost-catches-signal" + events per loop, each preceded by stutter (rapid opacity flips) and + tiny vertical jitter — like a CRT trying to sync. Max opacity ~13%, + always blurred — never crisp, always reads as buried in the noise. */ +@keyframes logo-tune-in { + /* — Long quiet — */ + 0%, 22% { opacity: 0; filter: blur(2px) brightness(0.9); transform: translate(-50%, -50%); } + + /* Event 1: stutter in, brief soft hold (~9% peak), stutter out */ + 22.3% { opacity: 0.06; filter: blur(1.4px) brightness(1.0); transform: translate(-50%, -50.3%); } + 22.6% { opacity: 0; filter: blur(2px) brightness(0.9); transform: translate(-50%, -49.8%); } + 23% { opacity: 0.09; filter: blur(0.8px) brightness(1.1); transform: translate(-50%, -50%); } + 24.6% { opacity: 0.09; filter: blur(0.8px) brightness(1.1); transform: translate(-50%, -50%); } + 25% { opacity: 0; filter: blur(2px) brightness(0.9); } + 25.3% { opacity: 0.04; } + 25.6% { opacity: 0; } + + /* — Long quiet — */ + 44% { opacity: 0; transform: translate(-50%, -50%); } + + /* Event 2: split-second flutter, signal almost catches but loses it (~5% peak) */ + 44.2% { opacity: 0.05; filter: blur(1.4px) brightness(1.0); transform: translate(-50.3%, -50%); } + 44.5% { opacity: 0; transform: translate(-50%, -50%); } + + /* — Quiet — */ + 68% { opacity: 0; transform: translate(-50%, -50%); filter: blur(2px) brightness(0.9); } + + /* Event 3: stuttered emergence to longer hold (~13% peak), stuttered exit */ + 68.3% { opacity: 0.05; filter: blur(2px) brightness(0.95); transform: translate(-49.7%, -50%); } + 68.6% { opacity: 0; } + 69% { opacity: 0.08; filter: blur(1px) brightness(1.05); transform: translate(-50%, -50.2%); } + 70.5% { opacity: 0.13; filter: blur(0.4px) brightness(1.15); transform: translate(-50%, -50%); } + 72% { opacity: 0.10; filter: blur(0.6px) brightness(1.1); } + 73% { opacity: 0; filter: blur(2px) brightness(0.9); } + 73.3% { opacity: 0.05; } + 73.6% { opacity: 0; } + + /* — Quiet to end — */ + 100% { opacity: 0; transform: translate(-50%, -50%); } +} /* ───────── Overlays ───────── */ #static { @@ -183,6 +248,11 @@ body { + +
TRANSMISSION @@ -222,7 +292,7 @@ body {
CH 118.0 MHz - NTSC · 525i · MONO + — TELEMETRY — ▲ AUDIO ACTIVE
@@ -231,6 +301,11 @@ body {
+ + + diff --git a/landing/telemetry.json b/landing/telemetry.json new file mode 100644 index 0000000..4007284 --- /dev/null +++ b/landing/telemetry.json @@ -0,0 +1,41 @@ +{ + "collectedAt": "2026-04-25T22:10:52Z", + "cpu": { + "model": "AMD Ryzen", + "threads": 12 + }, + "mem": { + "totalGB": 32 + }, + "host": { + "kernel": "Arch Linux 6.18.23-1-lts" + }, + "gpu": { + "name": "NVIDIA GeForce RTX 3060 Ti", + "vramTotalMB": 8192 + }, + "obs": { + "profile": "Untitled", + "renderer": "OpenGL", + "outputMode": "Advanced", + "canvas": { "w": 2560, "h": 1336 }, + "output": { "w": 1920, "h": 1080 }, + "fps": 30, + "color": { + "format": "NV12", + "space": "709", + "range": "Partial" + }, + "stream": { + "encoder": "x264", + "rateControl": "CBR", + "bitrateKbps": null, + "keyintSec": 2, + "profile": "high" + }, + "audio": { + "sampleRateHz": 48000, + "channels": "Stereo" + } + } +} diff --git a/landing/telemetry.sh b/landing/telemetry.sh new file mode 100755 index 0000000..197a930 --- /dev/null +++ b/landing/telemetry.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# Static device + OBS config snapshot for the OBS Landing overlay. +# +# Two modes: +# ./telemetry.sh — wrap telemetry.json → telemetry.js +# (idempotent; preserves any hand edits to the JSON) +# ./telemetry.sh --collect — re-read hardware + OBS config, overwrite +# telemetry.json, then wrap → telemetry.js +# +# Use --collect after a hardware change, kernel update, or OBS settings change. +# For cosmetic tweaks just edit telemetry.json by hand and re-run with no args. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OBS_DIR="$(cd "$DIR/.." && pwd)" +JSON="$DIR/telemetry.json" +JS="$DIR/telemetry.js" + +sanitize() { + printf '%s' "${1:-}" \ + | tr -d '"\\' \ + | tr '\n\r\t' ' ' \ + | sed 's/ */ /g; s/^ //; s/ $//' +} +emit_str() { [[ -z "${1:-}" ]] && printf 'null' || printf '"%s"' "$1"; } +emit_num() { [[ -z "${1:-}" ]] && printf 'null' || printf '%s' "$1"; } + +# ── INI reader (section-scoped) ───────────────────── +ini_get() { + # Usage: ini_get FILE SECTION KEY → prints value (empty if missing) + local file="$1" section="$2" key="$3" + [[ -f "$file" ]] || return 0 + awk -v section="[$section]" -v key="$key" ' + $0 == section { in_section = 1; next } + /^\[/ { in_section = 0; next } + in_section && index($0, key"=") == 1 { + sub("^[^=]*=", "") + print + exit + } + ' "$file" +} + +# Friendlier names for the encoder ids OBS stores in basic.ini +prettify_encoder() { + case "${1:-}" in + obs_x264|x264) echo "x264";; + jim_nvenc|ffmpeg_nvenc|obs_nvenc_h264_tex|obs_nvenc_h264_soft) echo "NVENC H.264";; + obs_nvenc_hevc_tex|obs_nvenc_hevc_soft) echo "NVENC HEVC";; + obs_nvenc_av1_tex|obs_nvenc_av1_soft) echo "NVENC AV1";; + obs_qsv11) echo "QSV";; + *) echo "${1:-}";; + esac +} + +# Pull a top-level field out of a flat JSON object via python3. +json_field() { + local file="$1" field="$2" + [[ -f "$file" ]] || return 0 + python3 - "$file" "$field" <<'PY' 2>/dev/null || true +import json, sys +try: + with open(sys.argv[1]) as f: + d = json.load(f) + v = d.get(sys.argv[2]) + print('' if v is None else v) +except Exception: + pass +PY +} + +# ── OBS config block ──────────────────────────────── +collect_obs() { + local user_ini="$OBS_DIR/user.ini" + [[ -f "$user_ini" ]] || { echo "null"; return; } + + local profile renderer + profile=$(ini_get "$user_ini" "Basic" "Profile") + renderer=$(ini_get "$user_ini" "Video" "Renderer") + [[ -z "$profile" ]] && profile="Untitled" + + local profile_dir="$OBS_DIR/basic/profiles/$profile" + local ini="$profile_dir/basic.ini" + [[ -f "$ini" ]] || { echo "null"; return; } + + local mode base_w base_h out_w out_h fps + mode=$(ini_get "$ini" "Output" "Mode") + base_w=$(ini_get "$ini" "Video" "BaseCX") + base_h=$(ini_get "$ini" "Video" "BaseCY") + out_w=$(ini_get "$ini" "Video" "OutputCX") + out_h=$(ini_get "$ini" "Video" "OutputCY") + fps=$(ini_get "$ini" "Video" "FPSInt") + + local color_fmt color_space color_range + color_fmt=$(ini_get "$ini" "Video" "ColorFormat") + color_space=$(ini_get "$ini" "Video" "ColorSpace") + color_range=$(ini_get "$ini" "Video" "ColorRange") + + local sample_rate ch_setup + sample_rate=$(ini_get "$ini" "Audio" "SampleRate") + ch_setup=$(ini_get "$ini" "Audio" "ChannelSetup") + + # Stream encoder + bitrate. Path differs Simple vs Advanced. + local enc_raw="" bitrate="" rc="" keyint="" h264_profile="" + if [[ "$mode" == "Advanced" ]]; then + enc_raw=$(ini_get "$ini" "AdvOut" "Encoder") + local sej="$profile_dir/streamEncoder.json" + rc=$(json_field "$sej" "rate_control") + bitrate=$(json_field "$sej" "bitrate") + keyint=$(json_field "$sej" "keyint_sec") + h264_profile=$(json_field "$sej" "profile") + else + enc_raw=$(ini_get "$ini" "SimpleOutput" "StreamEncoder") + bitrate=$(ini_get "$ini" "SimpleOutput" "VBitrate") + fi + local enc_pretty + enc_pretty=$(prettify_encoder "$enc_raw") + + profile=$(sanitize "$profile") + renderer=$(sanitize "$renderer") + mode=$(sanitize "$mode") + color_fmt=$(sanitize "$color_fmt") + color_space=$(sanitize "$color_space") + color_range=$(sanitize "$color_range") + ch_setup=$(sanitize "$ch_setup") + enc_pretty=$(sanitize "$enc_pretty") + rc=$(sanitize "$rc") + h264_profile=$(sanitize "$h264_profile") + + cat </dev/null 2>&1; then + local q + q=$(nvidia-smi --query-gpu=name,memory.total \ + --format=csv,noheader,nounits 2>/dev/null | head -n1 || true) + if [[ -n "$q" ]]; then + IFS=',' read -r gpu_name gpu_vram_total_mb <<<"$q" + gpu_name=$(sanitize "$gpu_name") + gpu_vram_total_mb=$(printf '%s' "$gpu_vram_total_mb" | tr -d ' ') + fi + fi + + cpu_model=$(sanitize "$cpu_model") + kernel=$(sanitize "$kernel") + local now_iso; now_iso=$(date -u +%FT%TZ) + + local obs_block; obs_block=$(collect_obs) + + local tmp="$JSON.tmp.$$" + cat > "$tmp" </dev/null 2>&1; then + echo "collect: produced invalid JSON, leaving $tmp for inspection" >&2 + exit 1 + fi + mv -f "$tmp" "$JSON" + echo "wrote $JSON" +} + +# Wraps telemetry.json as `window.__TEL = {...};` so index.html can load it +# via a