diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eba00e1..a565f96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,25 +31,26 @@ jobs: libgl-dev \ python3 - - name: Compile EverLive Userspace Engine (GCC) - run: | - make -C tools/opengc573-userd -j"$(nproc)" - make -C tools/opengc570d-userd CC=gcc -j"$(nproc)" - make -C tools/opengc573-userd clean - make -C tools/opengc570d-userd clean + - name: Compile everything (GCC) + run: make -j"$(nproc)" - - name: Compile EverLive Userspace Engine (Clang) - run: | - make -C tools/opengc573-userd CC=clang -j"$(nproc)" - make -C tools/opengc570d-userd CC=clang -j"$(nproc)" + - name: Unit tests (GCC) + run: make -C tests clean test - name: Frame generation cadence self-test - run: | - make -C tools/opengc573-userd selftest + run: make -C tools/opengc573-userd framegen-selftest - - name: OpenGC570D EDID & config unit tests + - name: Compile everything (Clang) run: | - make -C tools/opengc570d-userd selftest + make clean + make CC=clang -j"$(nproc)" + make CC=clang -C tests clean test + + - name: Sanitizer run + run: make -C tests asan + + - name: Clean + run: make clean - name: Verify Python syntax run: | @@ -66,3 +67,4 @@ jobs: bash -n tools/opengc573-userd/vfio_pci_bind.sh bash -n tools/opengc570d-userd/gc570d-vfio-bind.sh bash -n tools/opengc570d-userd/gc570d-sleep.sh + bash -n tools/libeverlive-core/everlive-service.sh diff --git a/.gitignore b/.gitignore index 54469a5..d9d6512 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.ko *.mod.c *.o +*.d *.symvers *.order *.cmd @@ -42,7 +43,17 @@ tools/opengc570d-userd/libgc570d-v4l2.so # Test binaries tests/test_edid tests/test_config -tools/opengc573-userd/gc573_framegen_selftest +tests/test_card_registry +tests/test_frame_convert +tests/test_ctl_proto +tests/test_json_rpc +tests/test_framegen + +# libeverlive-core build outputs +tools/libeverlive-core/*.a +tools/libeverlive-core/*.so* +tools/libeverlive-core/everlive-ctl +tools/libeverlive-core/**/*.d # Raw uncompressed video dumps 20261010_131220.mp4 diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf0ce5..43d6323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] + +### Added +- **Shared `libeverlive-core`:** card registry and subsystem-aware autodiscovery, YUY2 frame conversion, jsmn-backed JSON-RPC helpers, control-socket path resolution, and the `everlive-ctl` CLI. +- **Unified control plane for the GC570D:** the DUO daemon now serves the same newline-delimited JSON-RPC 2.0 protocol on `everlive.sock` (status, LED, audio, OSD, aspect/scale, input switching, HPD, EDID policy) alongside the legacy `gc570d-ctl` socket. + +### Changed +- **GC573 layout:** sources moved to `tools/opengc573-userd/src/`; the build links `libeverlive_core.a` and only applies SDL/FFmpeg/PipeWire flags to the translation units that need them. +- **Build and install:** a top-level `Makefile` orchestrates the core and both engines; `build.sh`, `install.sh`, and `uninstall.sh` now install and remove `everlive-ctl`, the core library and headers, and the universal `everlive.service` unit. +- **Launchers:** `play.sh` and `everlive-service.sh` share `everlive-ctl detect` instead of duplicating PCI ID logic. + +### Fixed +- The GC573 engine no longer claims a GC570D that shares PCI ID `1461:0054`; discovery is subsystem-aware and fails safe when the subsystem ID is unreadable. +- `everlive-ctl` connects to the live `everlive.sock` instead of the stale `everlive-experimental.sock` path. + +### Tests +- Unified `tests/` suite linking the core archive, `test_json_rpc` (renamed from `test_ipc_json`), the framegen cadence test, `make -C tests asan`, and `run_tests.sh --asan`. + ## [1.5.0] - 2026-11-10 ### Added diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6272cf8 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EverLive — top-level orchestration. +# Build order: libeverlive-core, then both engines, then tests. + +CORE_DIR := tools/libeverlive-core +GC573_DIR := tools/opengc573-userd +GC570D_DIR := tools/opengc570d-userd +TESTS_DIR := tests + +.PHONY: all core gc573 gc570d test check asan clean install uninstall + +all: core gc573 gc570d + +core: + +$(MAKE) -C $(CORE_DIR) + +gc573: core + +$(MAKE) -C $(GC573_DIR) + +gc570d: core + +$(MAKE) -C $(GC570D_DIR) + +test: all + +$(MAKE) -C $(TESTS_DIR) test + +check: test + +asan: all + +$(MAKE) -C $(TESTS_DIR) asan + +clean: + +$(MAKE) -C $(CORE_DIR) clean + +$(MAKE) -C $(GC573_DIR) clean + +$(MAKE) -C $(GC570D_DIR) clean + +$(MAKE) -C $(TESTS_DIR) clean + +install: all + +$(MAKE) -C $(CORE_DIR) install + +$(MAKE) -C $(GC573_DIR) install + +$(MAKE) -C $(GC570D_DIR) install + +uninstall: + +$(MAKE) -C $(CORE_DIR) uninstall + +$(MAKE) -C $(GC573_DIR) uninstall + +$(MAKE) -C $(GC570D_DIR) uninstall diff --git a/README.md b/README.md index 746e7a2..1d7681b 100644 --- a/README.md +++ b/README.md @@ -251,12 +251,21 @@ sudo ./install.sh --no-loopback `--skip-loopback` is an alias, and `sudo ./install.sh --help` lists all options. Without `v4l2loopback`, `/dev/video10` is unavailable — capture the game window via PipeWire instead (see [Streaming & Recording with OBS](#-streaming--recording-with-obs)). Re-running the installer without the flag re-enables the Virtual Camera. +### Building and testing from source +```bash +make -j"$(nproc)" # libeverlive-core + both engines +make -C tests test # 7 unit-test suites +make -C tests asan # sanitizer run +``` + +`./build.sh` is a thin wrapper around the same top-level `make`. + ### What the installer does: 1. Installs the build/runtime dependencies: SDL2, FFmpeg development libraries (`libavcodec`/`libswresample`/`libavutil`), `libcap` (`setcap`), PipeWire/WirePlumber, `xcb-util-cursor`, PySide6, standard build tools, optional `lsfg-vk` frame generation layer (skip with `--no-framegen`), and `v4l2loopback` where packaged (skip with `--no-loopback`). 2. Configures udev rules (`/etc/udev/rules.d/70-everlive.rules`) and early-boot VFIO autobind (`/etc/modules-load.d/everlive-vfio.conf` + `/etc/modprobe.d/everlive-vfio.conf`) granting your local user read/write access to the card's VFIO group and binding `vfio-pci`. 3. Sets `/etc/security/limits.d/99-everlive-memlock.conf` and adds `cap_ipc_lock` capabilities so unprivileged users can pin DMA buffers required for native 4K and dual-HDMI capture. 4. Blacklists the legacy out-of-tree `open_gc573` module (`/etc/modprobe.d/blacklist-everlive-legacy.conf`) so it cannot race with `vfio-pci` at boot. -5. Compiles userspace engines: `opengc573-userd` (`gc573_user_streamer`, `everlive_userd`, `gc573_user_probe`, `gc573_user_diag`) and `opengc570d-userd` (`opengc570d-userd`, `gc570d-ctl`, `libgc570d-v4l2.so`). +5. Compiles the shared core and userspace engines through the top-level `Makefile`: `libeverlive-core` (`everlive-ctl`, `libeverlive_core.so`), `opengc573-userd` (`gc573_user_streamer`, `everlive_userd`, `gc573_user_probe`, `gc573_user_diag`) and `opengc570d-userd` (`opengc570d-userd`, `gc570d-ctl`, `libgc570d-v4l2.so`). The core library, its headers, `everlive-ctl`, `everlive-service`, and the universal `everlive.service` unit are installed system-wide. 6. Configures an isolated PipeWire audio sink (`50-everlive-capture.conf`) providing the dedicated `"EverLive Capture Audio"` source for OBS. 7. Configures the Virtual Camera: `/etc/modprobe.d/everlive-v4l2loopback.conf` (`video_nr=10,11 card_label="EverLive Capture,EverLive Capture HDMI 2" exclusive_caps=0`) and loads the module at boot via `/etc/modules-load.d/`. 8. Installs the desktop application launcher and icon. @@ -361,6 +370,7 @@ Writing clean-room Linux drivers for proprietary capture hardware requires havin | Component | Path | License | Notes | |:---|:---|:---:|:---| | **Userspace Driver & Streamer** | `tools/opengc573-userd/` | **GPL-2.0-only** | Userspace VFIO implementation. Copyright © 2026 Everlite. | +| **Shared Core Library** | `tools/libeverlive-core/` | **GPL-2.0-only** | Card registry and autodiscovery, YUY2 frame conversion, jsmn-backed JSON-RPC helpers, control-socket resolution, `everlive-ctl`, and the `everlive.service` unit. Copyright © 2026 Everlite. | | **Live Gamer DUO Engine** | `tools/opengc570d-userd/` | **GPL-2.0-only** | Derived from [`neoyagami/OpenGC570D`](https://github.com/neoyagami/OpenGC570D) with userspace VFIO-PCI + PipeWire port by [@PolygonPrincess](https://github.com/PolygonPrincess). Upstream base `4673bb9`, splitter PR #6 `484aa8d`, userspace import `afbb06e`. EverLive integration and frontend © 2026 Everlite & PolygonPrincess. | | **EverLive Console Hub** | `tools/everlive/`, `play.sh`, `install.sh` | **GPL-2.0-only** | Python/Qt hub and launcher scripts. Copyright © 2026 Everlite. | | **Legacy Kernel Module** | `legacy/open_gc573-kernel/` | **GPL-2.0-only** | Historical kernel module based on work by ostrich, extended by Everlite. | diff --git a/build.sh b/build.sh index 567cdbf..aeadf3d 100755 --- a/build.sh +++ b/build.sh @@ -5,21 +5,22 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" -GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" -echo "[*] Baue EverLive GC573 Userspace VFIO Engine (gc573_user_streamer & everlive_userd)..." -make -C "${GC573_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC573_DIR}" -j"$(nproc)" +echo "[*] Building EverLive core library (libeverlive-core & everlive-ctl)..." +echo "[*] Building GC573 (Live Gamer 4K) userspace VFIO engine..." +echo "[*] Building GC570D (Live Gamer DUO) userspace engine..." +make -C "${SCRIPT_DIR}" -j"$(nproc)" -echo "[*] Baue EverLive GC570D (Live Gamer DUO) Userspace Engine..." -make -C "${GC570D_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC570D_DIR}" -j"$(nproc)" +echo "[+] EverLive Userspace Engines built successfully!" -echo "[+] EverLive Userspace Engines erfolgreich gebaut!" - -# Optional: Baue Legacy Direct-Renderer falls Quellcode vorhanden +# Optional: build the legacy direct renderer if its source is present if [ -f "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ]; then - echo "[*] Baue Legacy Direct-Renderer (everlive_display)..." - gcc -O3 -march=native -o "${SCRIPT_DIR}/tools/everlive/everlive_display" "${SCRIPT_DIR}/tools/everlive/everlive_display.c" $(sdl2-config --cflags --libs 2>/dev/null || true) -lGL -lm -lX11 -lpthread 2>/dev/null || true + echo "[*] Building legacy direct renderer (everlive_display)..." + SDL_FLAGS="$(pkg-config --cflags --libs sdl2 2>/dev/null || sdl2-config --cflags --libs 2>/dev/null || true)" + if [ -n "${SDL_FLAGS}" ]; then + gcc -O3 -o "${SCRIPT_DIR}/tools/everlive/everlive_display" \ + "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ${SDL_FLAGS} \ + -lGL -lm -lX11 -lpthread + chmod 0755 "${SCRIPT_DIR}/tools/everlive/everlive_display" 2>/dev/null || true + fi fi diff --git a/install.sh b/install.sh index 6e74b03..2d6f3af 100755 --- a/install.sh +++ b/install.sh @@ -12,6 +12,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" USERD_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" VIEWER_DIR="${SCRIPT_DIR}/tools/everlive" @@ -395,20 +396,32 @@ detect_and_install_deps # 3. Userspace VFIO Engine kompilieren echo -e "\n${BLUE}[2/5] Kompiliere EverLive Userspace VFIO Engine...${NC}" -make -C "${USERD_DIR}" clean >/dev/null 2>&1 || true -make -C "${USERD_DIR}" -j"$(nproc)" -echo -e "${BLUE}[*] Kompiliere GC570D (Live Gamer DUO) Userspace Engine...${NC}" -make -C "${GC570D_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC570D_DIR}" -j"$(nproc)" -# Never leave build artifacts world-writable, and keep the binary setcap-safe. -chmod 0755 "${USERD_DIR}/gc573_user_streamer" "${USERD_DIR}/everlive_userd" "${USERD_DIR}/gc573_user_probe" \ - "${GC570D_DIR}/opengc570d-userd" "${GC570D_DIR}/gc570d-ctl" "${SCRIPT_DIR}/play.sh" +make -C "${SCRIPT_DIR}" -j"$(nproc)" + +# Never leave build artifacts world-writable, and keep binaries setcap-safe. +chmod 0755 "${USERD_DIR}/gc573_user_streamer" "${USERD_DIR}/everlive_userd" \ + "${USERD_DIR}/gc573_user_probe" "${CORE_DIR}/everlive-ctl" \ + "${GC570D_DIR}/opengc570d-userd" "${GC570D_DIR}/gc570d-ctl" \ + "${SCRIPT_DIR}/play.sh" chmod 0644 "${GC570D_DIR}/libgc570d-v4l2.so" 2>/dev/null || true + +# Core library, everlive-ctl, everlive-service, headers, and systemd unit. +make -C "${CORE_DIR}" install + +# Install the GC573 engine binaries into /usr/local/bin with cap_ipc_lock. +make -C "${USERD_DIR}" install + # Install the GC570D helpers (udev rule, memlock, systemd unit, sleep hook) # through the engine's own Makefile so both installers stay in sync. make -C "${GC570D_DIR}" install + +# GC573 currently ships its bind helper from install.sh. +install -m 0755 "${USERD_DIR}/vfio_pci_bind.sh" /usr/local/bin/vfio_pci_bind.sh + setcap cap_ipc_lock=+ep "${USERD_DIR}/gc573_user_streamer" 2>/dev/null || true +setcap cap_ipc_lock=+ep /usr/local/bin/gc573_user_streamer 2>/dev/null || true setcap cap_ipc_lock=+ep "${GC570D_DIR}/opengc570d-userd" 2>/dev/null || true +setcap cap_ipc_lock=+ep /usr/local/bin/opengc570d-userd 2>/dev/null || true echo -e "${GREEN}[+] Userspace VFIO Engines erfolgreich gebaut & berechtigt!${NC}" # Python GUI-Umgebung (PySide6) für Benutzer prüfen und bei Bedarf einrichten diff --git a/play.sh b/play.sh index 090b6f7..11605e2 100755 --- a/play.sh +++ b/play.sh @@ -7,32 +7,59 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" +CORE_CTL="${CORE_DIR}/everlive-ctl" GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" VIEWER="${SCRIPT_DIR}/tools/everlive/everlive_viewer.py" # 0. Detect the installed capture card. GC573 and GC570D share PCI device -# 1461:0054; only the subsystem device ID distinguishes them. -CARD_MODEL="gc573" +# 1461:0054; only the subsystem device ID distinguishes them. Prefer the +# core registry and fall back to sysfs when everlive-ctl is unavailable. +detect_card_sysfs() { + local dev vendor device subsystem + + CARD_MODEL="" + CARD_PCI="" + for dev in /sys/bus/pci/devices/*; do + [ -f "$dev/vendor" ] || continue + [ -f "$dev/device" ] || continue + vendor=$(cat "$dev/vendor" 2>/dev/null || true) + device=$(cat "$dev/device" 2>/dev/null || true) + if [ "$vendor" != "0x1461" ]; then + continue + fi + if [ "$device" != "0x0054" ] && [ "$device" != "0x0055" ]; then + continue + fi + CARD_PCI=$(basename "$dev") + subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) + if [ "$device" = "0x0054" ] && [ "$subsystem" = "0x5700" ]; then + CARD_MODEL="gc570d" + else + CARD_MODEL="gc573" + fi + break + done +} + +CARD_MODEL="" CARD_PCI="" -for dev in /sys/bus/pci/devices/*; do - [ -f "$dev/vendor" ] || continue - [ -f "$dev/device" ] || continue - vendor=$(cat "$dev/vendor" 2>/dev/null || true) - device=$(cat "$dev/device" 2>/dev/null || true) - if [ "$vendor" != "0x1461" ]; then - continue - fi - if [ "$device" != "0x0054" ] && [ "$device" != "0x0055" ]; then - continue - fi - CARD_PCI=$(basename "$dev") - subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) - if [ "$device" = "0x0054" ] && [ "$subsystem" = "0x5700" ]; then - CARD_MODEL="gc570d" - fi - break -done +if [ -x "${CORE_CTL}" ]; then + DETECT_OUT="$("${CORE_CTL}" detect 2>/dev/null || true)" + CARD_MODEL="$(printf '%s\n' "${DETECT_OUT}" \ + | sed -n 's/.*Model Slug:[[:space:]]*//p' | head -n 1)" + CARD_PCI="$(printf '%s\n' "${DETECT_OUT}" \ + | sed -n 's/.*PCI BDF:[[:space:]]*//p' | head -n 1)" + case "${CARD_MODEL}" in + gc573|gc570d) ;; + *) CARD_MODEL="" ;; + esac +fi +if [ -z "${CARD_MODEL}" ]; then + detect_card_sysfs +fi +CARD_MODEL="${CARD_MODEL:-gc573}" if [ "$CARD_MODEL" = "gc570d" ]; then USERD_DIR="${GC570D_DIR}" @@ -48,8 +75,14 @@ else LOOPBACK_LABEL_2="" fi -# 1. Ensure userspace engine binaries are built and up to date -if ! make -C "${USERD_DIR}" -q >/dev/null 2>&1; then +# 1. Ensure the core and userspace engine binaries are built and up to date +if [ ! -x "${CORE_CTL}" ] || \ + ! make -q -C "${CORE_DIR}" everlive-ctl >/dev/null 2>&1; then + echo "[*] Aktualisiere/Kompiliere EverLive Core Library..." + make -C "${CORE_DIR}" -j"$(nproc)" +fi +if [ ! -x "${STREAMER_BIN}" ] || \ + ! make -q -C "${USERD_DIR}" "$(basename "${STREAMER_BIN}")" >/dev/null 2>&1; then echo "[*] Aktualisiere/Kompiliere EverLive Userspace VFIO Engine..." make -C "${USERD_DIR}" -j"$(nproc)" fi diff --git a/tests/Makefile b/tests/Makefile index 2389e4b..8a68d82 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,35 +1,83 @@ # SPDX-License-Identifier: GPL-2.0-only +# +# EverLive unit tests: libeverlive-core, GC570D, and GC573. +# Core tests link libeverlive_core.a; engine tests compile only the modules +# they exercise. + CC ?= gcc -CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE -INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/opengc573-userd -DEFS := -DGC570D_USERSPACE +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare \ + -std=gnu11 -D_GNU_SOURCE +DEPFLAGS := -MMD -MP + +CORE_DIR := ../tools/libeverlive-core +GC573_DIR := ../tools/opengc573-userd +GC570D_DIR := ../tools/opengc570d-userd +GC570D_SRC := $(GC570D_DIR)/src + +INCLUDES := -I$(CORE_DIR) -I$(GC570D_SRC) -I$(GC570D_DIR) -I$(GC573_DIR)/src +DEFS_GC570D := -DGC570D_USERSPACE -GC570D_DIR := ../tools/opengc570d-userd/src -GC573_DIR := ../tools/opengc573-userd +CORE_LIB := $(CORE_DIR)/libeverlive_core.a +SANITIZE := -fsanitize=address,undefined -fno-omit-frame-pointer -TARGETS := test_edid test_config test_ipc_json +TARGETS := test_card_registry test_frame_convert test_ctl_proto test_json_rpc \ + test_edid test_config test_framegen all: $(TARGETS) -test_edid: test_edid.c $(GC570D_DIR)/gc570d-receiver.c $(GC570D_DIR)/gc570d-splitter.c - $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ +$(CORE_LIB): FORCE + $(MAKE) -C $(CORE_DIR) + +.PHONY: FORCE + +# --- libeverlive-core --------------------------------------------------- +test_card_registry: test_card_registry.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -test_config: test_config.c $(GC570D_DIR)/gc570d-config.c - $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ +test_frame_convert: test_frame_convert.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -test_ipc_json: test_ipc_json.c $(GC573_DIR)/third_party/jsmn.c - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread +test_ctl_proto: test_ctl_proto.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -asan: all +test_json_rpc: test_json_rpc.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) + +# --- GC570D ------------------------------------------------------------- +# These two provide their own kernel-shim stubs, so they must compile the +# engine modules directly and must NOT link the core archive. +test_edid: test_edid.c $(GC570D_SRC)/gc570d-receiver.c \ + $(GC570D_SRC)/gc570d-splitter.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEFS_GC570D) -o $@ $^ + +test_config: test_config.c $(GC570D_SRC)/gc570d-config.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEFS_GC570D) -o $@ $^ + +# --- GC573 -------------------------------------------------------------- +test_framegen: test_framegen.c $(GC573_DIR)/src/gc573_user_framegen.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread -lm + +-include $(TARGETS:=.d) test: all - @echo "Running GC570D & GC573 unit tests..." + @echo "=== libeverlive-core ===" + @./test_card_registry + @./test_frame_convert + @./test_ctl_proto + @./test_json_rpc + @echo "=== GC570D (Live Gamer DUO) ===" @./test_edid @./test_config - @./test_ipc_json + @echo "=== GC573 (Live Gamer 4K) ===" + @./test_framegen + @echo "=== All EverLive tests passed ===" + +asan: + $(MAKE) clean + $(MAKE) CFLAGS="$(CFLAGS) $(SANITIZE)" all + $(MAKE) CFLAGS="$(CFLAGS) $(SANITIZE)" test clean: - rm -f $(TARGETS) + rm -f $(TARGETS) $(TARGETS:=.d) -.PHONY: all test clean asan +.PHONY: all test asan clean diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 4224ff5..a891535 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,11 +1,25 @@ #!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-only +# +# Build and run the EverLive unit tests (libeverlive-core, GC570D, GC573). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODE="${1:-}" -echo "=== Building and running GC570D Unit Tests ===" -make -C "${SCRIPT_DIR}" clean -make -C "${SCRIPT_DIR}" test -echo "=== All tests completed successfully! ===" - +case "${MODE}" in + "") + make -C "${SCRIPT_DIR}" clean + make -C "${SCRIPT_DIR}" test + ;; + --asan) + make -C "${SCRIPT_DIR}" asan + ;; + -h|--help) + echo "Usage: $0 [--asan]" + ;; + *) + echo "Unknown option: ${MODE}" >&2 + exit 2 + ;; +esac diff --git a/tests/test_card_registry.c b/tests/test_card_registry.c new file mode 100644 index 0000000..bedfecf --- /dev/null +++ b/tests/test_card_registry.c @@ -0,0 +1,396 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_card_registry.c — Unit tests for libeverlive-core hardware registry and autodiscovery + */ + +#include "everlive_card.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TEST_PASS(desc) printf(" [PASS] %s\n", (desc)) + +static void test_registry_enumeration(void) +{ + printf("Testing everlive_get_card_registry...\n"); + size_t count = 0; + const struct everlive_device_desc *registry = everlive_get_card_registry(&count); + + assert(registry != NULL); + assert(count >= 3); + TEST_PASS("Registry contains multiple supported card definitions"); + + bool found_gc570d = false; + bool found_gc573 = false; + + for (size_t i = 0; i < count; i++) { + if (strcmp(registry[i].model_slug, "gc570d") == 0) + found_gc570d = true; + if (strcmp(registry[i].model_slug, "gc573") == 0) + found_gc573 = true; + } + + assert(found_gc570d); + assert(found_gc573); + TEST_PASS("Registry includes both GC570D and GC573 descriptors"); +} + +static void test_card_matching(void) +{ + printf("Testing everlive_match_card...\n"); + + /* Exact GC570D match (0x1461, 0x0054, 0x5700) */ + const struct everlive_device_desc *gc570d = + everlive_match_card(0x1461, 0x0054, 0x5700); + assert(gc570d != NULL); + assert(strcmp(gc570d->model_slug, "gc570d") == 0); + assert(gc570d->num_input_ports == 2); + assert(gc570d->capabilities & EVERLIVE_CAP_DUAL_INPUT); + assert(gc570d->capabilities & EVERLIVE_CAP_HARDWARE_SPLITTER); + TEST_PASS("Identified GC570D by subsystem 0x5700 with dual inputs"); + + /* Exact GC573 match (0x1461, 0x0054, 0x5730) */ + const struct everlive_device_desc *gc573_exact = + everlive_match_card(0x1461, 0x0054, 0x5730); + assert(gc573_exact != NULL); + assert(strcmp(gc573_exact->model_slug, "gc573") == 0); + assert(gc573_exact->capabilities & EVERLIVE_CAP_4K60); + assert(gc573_exact->capabilities & EVERLIVE_CAP_1440P120); + assert(gc573_exact->capabilities & EVERLIVE_CAP_FRAMEGEN); + TEST_PASS("Identified GC573 by subsystem 0x5730 with 4K60/1440p120/FrameGen"); + + /* Wildcard GC573 match (0x1461, 0x0054 with non-5700 subsystem) */ + const struct everlive_device_desc *gc573_wildcard = + everlive_match_card(0x1461, 0x0054, 0x0000); + assert(gc573_wildcard != NULL); + assert(strcmp(gc573_wildcard->model_slug, "gc573") == 0); + TEST_PASS("Defaulted 0x0054 to GC573 when subsystem != 0x5700"); + + /* Arbitrary non-0x5700 subsystem uses the data-driven fallback entry. */ + const struct everlive_device_desc *gc573_fallback = + everlive_match_card(0x1461, 0x0054, 0x1234); + assert(gc573_fallback != NULL); + assert(strcmp(gc573_fallback->model_slug, "gc573") == 0); + TEST_PASS("Fallback match is driven by the registry, not hard-coded IDs"); + + /* Secondary GC573 / BOLT PCIe ID (0x0055) */ + const struct everlive_device_desc *gc573_alt = + everlive_match_card(0x1461, 0x0055, 0x0000); + assert(gc573_alt != NULL); + assert(strcmp(gc573_alt->model_slug, "gc573") == 0); + TEST_PASS("Identified alternative GC573/BOLT PCIe device ID 0x0055"); + + /* Future GC575 template (0x0057) */ + const struct everlive_device_desc *gc575 = + everlive_match_card(0x1461, 0x0057, 0x0000); + assert(gc575 != NULL); + assert(strcmp(gc575->model_slug, "gc575") == 0); + TEST_PASS("Identified GC575 device ID 0x0057"); + + /* Unknown card rejected */ + const struct everlive_device_desc *unknown = + everlive_match_card(0x10de, 0x1f02, 0x0000); + assert(unknown == NULL); + TEST_PASS("Unknown PCI IDs rejected gracefully"); +} + +static void create_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + assert(f != NULL); + fputs(content, f); + fclose(f); +} + +static int remove_entry(const char *path, const struct stat *sb, int typeflag, + struct FTW *ftwbuf) +{ + (void)sb; + (void)typeflag; + (void)ftwbuf; + return remove(path); +} + +static void test_mock_sysfs_scan(void) +{ + printf("Testing everlive_detect_installed_cards with mock sysfs...\n"); + + char tmp_root[] = "/tmp/everlive_sysfs_test_XXXXXX"; + char *res = mkdtemp(tmp_root); + assert(res != NULL); + + /* Mock device 1: GC570D at 0000:03:00.0 */ + char d1[512], d2[512], d3[512], d4[512]; + snprintf(d1, sizeof(d1), "%s/0000:03:00.0", tmp_root); + snprintf(d2, sizeof(d2), "%s/0000:04:00.0", tmp_root); + snprintf(d3, sizeof(d3), "%s/0000:01:00.0", tmp_root); + snprintf(d4, sizeof(d4), "%s/0000:05:00.0", tmp_root); + + assert(mkdir(d1, 0755) == 0); + assert(mkdir(d2, 0755) == 0); + assert(mkdir(d3, 0755) == 0); + assert(mkdir(d4, 0755) == 0); + + /* Populate GC570D */ + char p[PATH_MAX]; + snprintf(p, sizeof(p), "%s/vendor", d1); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d1); + create_file(p, "0x0054\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d1); + create_file(p, "0x5700\n"); + + /* Mock symlink for driver -> vfio-pci */ + snprintf(p, sizeof(p), "%s/driver", d1); + assert(symlink("/sys/bus/pci/drivers/vfio-pci", p) == 0); + + /* Mock symlink for iommu_group -> .../16 */ + snprintf(p, sizeof(p), "%s/iommu_group", d1); + assert(symlink("/sys/kernel/iommu_groups/16", p) == 0); + + /* Populate GC573 at 0000:04:00.0 */ + snprintf(p, sizeof(p), "%s/vendor", d2); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d2); + create_file(p, "0x0054\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d2); + create_file(p, "0x5730\n"); + + /* Mock symlink for iommu_group -> .../22 */ + snprintf(p, sizeof(p), "%s/iommu_group", d2); + assert(symlink("/sys/kernel/iommu_groups/22", p) == 0); + + /* Populate other non-capture device at 0000:01:00.0 (e.g. Intel GPU) */ + snprintf(p, sizeof(p), "%s/vendor", d3); + create_file(p, "0x8086\n"); + snprintf(p, sizeof(p), "%s/device", d3); + create_file(p, "0x9bc5\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d3); + create_file(p, "0x103c\n"); + + /* 0x1461:0x0054 with NO subsystem_device file. A read failure must not + * be treated as subsystem 0 and silently guessed as a GC573. + */ + snprintf(p, sizeof(p), "%s/vendor", d4); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d4); + create_file(p, "0x0054\n"); + + /* Scan using mock root */ + struct everlive_device found[8]; + int count = everlive_detect_installed_cards(found, 8, tmp_root); + + assert(count == 2); + TEST_PASS("Discovered exactly 2 supported capture cards from mock sysfs"); + + bool saw_gc570d = false; + bool saw_gc573 = false; + + for (int i = 0; i < count; i++) { + if (strcmp(found[i].desc->model_slug, "gc570d") == 0) { + saw_gc570d = true; + assert(strcmp(found[i].pci_bdf, "0000:03:00.0") == 0); + assert(found[i].iommu_group == 16); + assert(found[i].is_vfio_bound == true); + } + if (strcmp(found[i].desc->model_slug, "gc573") == 0) { + saw_gc573 = true; + assert(strcmp(found[i].pci_bdf, "0000:04:00.0") == 0); + assert(found[i].iommu_group == 22); + assert(found[i].is_vfio_bound == false); + } + } + + assert(saw_gc570d); + assert(saw_gc573); + TEST_PASS("Correctly populated BDF, IOMMU group, and VFIO binding state"); + + /* The device with an unreadable subsystem ID must be skipped rather than + * misidentified as a GC573 via the fallback path. */ + for (int i = 0; i < count; i++) + assert(strcmp(found[i].pci_bdf, "0000:05:00.0") != 0); + TEST_PASS("Missing subsystem_device does not misidentify a GC570D"); + + /* Cleanup mock directory. FTW_PHYS keeps nftw from following the + * driver/iommu_group symlinks into the real sysfs tree. */ + assert(nftw(tmp_root, remove_entry, 16, FTW_DEPTH | FTW_PHYS) == 0); +} + +static int g_custom_probe_called = 0; +static int mock_custom_probe(struct everlive_device *dev) +{ + (void)dev; + g_custom_probe_called++; + return 0; +} + +/* + * Register a vtable that lives only for this call. The core must copy it, + * otherwise the binding table is left pointing at a dead stack frame. This + * runs before any lookup, because the first lookup freezes the registry. + */ +static void register_ops_from_stack(void) +{ + const struct everlive_device_desc *unbound = + everlive_match_card(0x1461, 0x0054, 0x5700); /* GC570D */ + struct everlive_card_ops stack_ops; + + assert(unbound != NULL && unbound->ops != NULL); + stack_ops = *unbound->ops; + stack_ops.probe = mock_custom_probe; + assert(everlive_register_card_ops("gc573", &stack_ops) == 0); +} + +static void test_card_ops_binding(void) +{ + const struct everlive_device_desc *gc573_desc = + everlive_match_card(0x1461, 0x0054, 0x5730); + const struct everlive_device_desc *gc570d_desc = + everlive_match_card(0x1461, 0x0054, 0x5700); + struct everlive_device dev; + const struct everlive_card_ops *bound; + const struct everlive_card_ops *default_ops; + + printf("Testing everlive_card_ops binding and defaults...\n"); + assert(gc573_desc != NULL && gc570d_desc != NULL); + + /* 1. Register before any lookup; the first lookup freezes the table. */ + register_ops_from_stack(); + + /* 2. The copied vtable is visible and outlives the caller's stack. */ + bound = everlive_card_ops_for(gc573_desc); + assert(bound != NULL); + assert(bound->probe == mock_custom_probe); + + memset(&dev, 0, sizeof(dev)); + g_custom_probe_called = 0; + assert(bound->probe(&dev) == 0); + assert(g_custom_probe_called == 1); + TEST_PASS("Registered vtable is copied and outlives the caller's stack"); + + /* 3. Registration is closed once a lookup has happened. */ + assert(everlive_register_card_ops("gc573", bound) == -EBUSY); + TEST_PASS("Registry freezes after the first ops lookup"); + + /* 4. An unbound model still falls back to the built-in defaults. */ + default_ops = everlive_card_ops_for(gc570d_desc); + assert(default_ops != NULL); + assert(default_ops->probe != NULL); + assert(default_ops->init_hardware != NULL); + assert(default_ops->standby != NULL); + assert(default_ops->release != NULL); + assert(default_ops->set_input_port != NULL); + assert(default_ops->set_edid_profile != NULL); + assert(default_ops->enable_passthrough != NULL); + assert(default_ops->set_led != NULL); + assert(default_ops->get_telemetry != NULL); + TEST_PASS("Default ops returned for an unbound model"); + + memset(&dev, 0, sizeof(dev)); + assert(default_ops->probe(&dev) == -EINVAL); + + dev.desc = gc570d_desc; + snprintf(dev.pci_bdf, sizeof(dev.pci_bdf), "0000:03:00.0"); + assert(default_ops->probe(&dev) == 0); + assert(default_ops->init_hardware(&dev) == -ENOTSUP); + assert(default_ops->standby(&dev) == -ENOTSUP); + assert(default_ops->set_input_port(&dev, 1) == -ENOTSUP); + assert(default_ops->set_edid_profile(&dev, 0, "default", NULL, 0) == -ENOTSUP); + assert(default_ops->enable_passthrough(&dev, true) == -ENOTSUP); + assert(default_ops->set_led(&dev, EVERLIVE_LED_MODE_STATIC, 255, 0, 0) == -ENOTSUP); + + struct everlive_telemetry telem; + memset(&telem, 0xaa, sizeof(telem)); + assert(default_ops->get_telemetry(&dev, &telem) == -ENOTSUP); + assert(telem.frames_captured == 0 && telem.temperature_c == 0.0f); + + default_ops->release(&dev); + TEST_PASS("Default ops behave safely and return -ENOTSUP for hardware actions"); + + /* 5. Invalid arguments are still reported after the freeze. */ + assert(everlive_register_card_ops(NULL, default_ops) == -EINVAL); + assert(everlive_register_card_ops("gc573", NULL) == -EINVAL); + assert(everlive_register_card_ops("nonexistent_model", default_ops) == -ENOENT); + TEST_PASS("Rejects NULL arguments and unknown model slugs for ops registration"); +} + +static uint64_t timespec_to_ns_test(const struct timespec *ts) +{ + return (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; +} + +static bool check_resume_detected(const struct timespec *mono_now, + const struct timespec *boot_now, + const struct timespec *mono_prev, + const struct timespec *boot_prev) +{ + uint64_t mono_delta = timespec_to_ns_test(mono_now) - timespec_to_ns_test(mono_prev); + uint64_t boot_delta = timespec_to_ns_test(boot_now) - timespec_to_ns_test(boot_prev); + + return (boot_delta > mono_delta + 2000000000ULL); +} + +static void test_resume_detection(void) +{ + printf("Testing system resume detection logic...\n"); + + struct timespec mono_prev = { .tv_sec = 100, .tv_nsec = 0 }; + struct timespec boot_prev = { .tv_sec = 500, .tv_nsec = 0 }; + + /* 1. Normal running: both monotonic and boottime advance equally by 250 ms */ + struct timespec mono_now = { .tv_sec = 100, .tv_nsec = 250000000 }; + struct timespec boot_now = { .tv_sec = 500, .tv_nsec = 250000000 }; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("Normal 250ms interval does not trigger resume"); + + /* 2. Jitter or scheduling delay: boot advances by 500ms, mono advances by 400ms */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 400000000; + boot_now.tv_sec = 500; boot_now.tv_nsec = 500000000; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("100ms jitter does not trigger resume"); + + /* 3. System suspend: machine sleeps for 10 seconds. + * monotonic stopped while sleeping (advanced by only 250ms awake time), + * boottime kept counting (advanced by 10.25 seconds). + */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 250000000; + boot_now.tv_sec = 510; boot_now.tv_nsec = 250000000; + assert(check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("10-second system sleep accurately triggers resume detection"); + + /* 4. Threshold boundary: exactly 2.0s delta is not triggered (> 2.0s required) */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 0; + boot_now.tv_sec = 502; boot_now.tv_nsec = 0; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + + /* 2.000000001s triggers */ + boot_now.tv_nsec = 1; + assert(check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("Resume detection threshold strictly enforced at > 2.0 seconds delta"); +} + +int main(void) +{ + printf("==================================================\n"); + printf(" libeverlive-core Card Registry & Discovery Tests \n"); + printf("==================================================\n"); + + test_registry_enumeration(); + test_card_matching(); + test_mock_sysfs_scan(); + test_card_ops_binding(); + test_resume_detection(); + + printf("==================================================\n"); + printf(" ALL CARD REGISTRY TESTS PASSED!\n"); + printf("==================================================\n"); + return 0; +} diff --git a/tests/test_ctl_proto.c b/tests/test_ctl_proto.c new file mode 100644 index 0000000..5c94342 --- /dev/null +++ b/tests/test_ctl_proto.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_ctl_proto.c — Unit tests for the everlive-ctl request helpers. + * + * These exercise the production serialization/validation code directly, so a + * change in everlive_ctl_proto.c cannot silently drift away from the tests. + */ + +#include "everlive_ctl.h" + +#include +#include +#include + +#define TEST_PASS(desc) printf(" [PASS] %s\n", (desc)) + +static void test_build_request(void) +{ + char req[512]; + char tiny[8]; + + printf("Testing everlive_ctl_build_request...\n"); + + assert(everlive_ctl_build_request("status.get", "{}", req, + sizeof(req)) == 0); + assert(strcmp(req, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"status.get\"," + "\"params\":{}}\n") == 0); + TEST_PASS("status.get request is well formed"); + + assert(everlive_ctl_build_request("hotplug", "{\"pulse\":true}", req, + sizeof(req)) == 0); + assert(strstr(req, "\"method\":\"hotplug\"") != NULL); + assert(strstr(req, "\"params\":{\"pulse\":true}") != NULL); + TEST_PASS("hotplug request preserves params"); + + /* NULL / empty params become an empty JSON object. */ + assert(everlive_ctl_build_request("status.get", NULL, req, + sizeof(req)) == 0); + assert(strstr(req, "\"params\":{}") != NULL); + TEST_PASS("NULL params default to {}"); + + /* Truncation must be reported, not silently accepted. */ + assert(everlive_ctl_build_request("status.get", "{}", tiny, + sizeof(tiny)) == -1); + TEST_PASS("Too-small buffer is rejected"); +} + +static void test_parse_double(void) +{ + double v; + + printf("Testing everlive_ctl_parse_double...\n"); + + assert(everlive_ctl_parse_double("85", &v) == 0 && v == 85.0); + assert(everlive_ctl_parse_double("-0.5", &v) == 0 && v == -0.5); + assert(everlive_ctl_parse_double("1e3", &v) == 0 && v == 1000.0); + TEST_PASS("Valid decimal numbers accepted"); + + assert(everlive_ctl_parse_double("nan", &v) == -1); + assert(everlive_ctl_parse_double("inf", &v) == -1); + assert(everlive_ctl_parse_double("-inf", &v) == -1); + assert(everlive_ctl_parse_double("1e999", &v) == -1); + TEST_PASS("NaN, infinity and overflow rejected"); + + assert(everlive_ctl_parse_double("abc", &v) == -1); + assert(everlive_ctl_parse_double("12abc", &v) == -1); + assert(everlive_ctl_parse_double("\"; rm -rf /", &v) == -1); + assert(everlive_ctl_parse_double("", &v) == -1); + assert(everlive_ctl_parse_double(NULL, &v) == -1); + TEST_PASS("Garbage and embedded JSON rejected"); +} + +static void test_parse_hex_rgb(void) +{ + unsigned long rgb; + + printf("Testing everlive_ctl_parse_hex_rgb...\n"); + + assert(everlive_ctl_parse_hex_rgb("FF8020", &rgb) == 0); + assert(rgb == 0xff8020UL); + assert(everlive_ctl_parse_hex_rgb("ff8020", &rgb) == 0); + assert(rgb == 0xff8020UL); + assert(everlive_ctl_parse_hex_rgb("0", &rgb) == 0 && rgb == 0); + TEST_PASS("Valid RRGGBB values accepted"); + + assert(everlive_ctl_parse_hex_rgb("GGGGGG", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("1000000", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("-1", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb(NULL, &rgb) == -1); + TEST_PASS("Garbage, overflow and negative values rejected"); +} + +static void test_json_escape(void) +{ + char out[128]; + char tiny[8]; + + printf("Testing everlive_ctl_json_escape...\n"); + + everlive_ctl_json_escape("plain", out, sizeof(out)); + assert(strcmp(out, "plain") == 0); + + everlive_ctl_json_escape("a\"b\\c\nd\te", out, sizeof(out)); + assert(strcmp(out, "a\\\"b\\\\c\\nd\\te") == 0); + TEST_PASS("quotes, backslashes and whitespace escaped"); + + everlive_ctl_json_escape("\x01", out, sizeof(out)); + assert(strcmp(out, "\\u0001") == 0); + TEST_PASS("control bytes escaped as \\uXXXX"); + + everlive_ctl_json_escape("abcdefghijklmnop", tiny, sizeof(tiny)); + assert(memchr(tiny, '\0', sizeof(tiny)) != NULL); + TEST_PASS("Truncated output stays NUL-terminated"); +} + +static void test_response_has_error(void) +{ + printf("Testing everlive_ctl_response_has_error...\n"); + + assert(everlive_ctl_response_has_error( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-1}}")); + assert(everlive_ctl_response_has_error("{ \"error\" : {} }")); + assert(everlive_ctl_response_has_error("{\"error\":null}")); + + /* The word "error" inside a value must not count as a failure. */ + assert(!everlive_ctl_response_has_error( + "{\"result\":{\"message\":\"No error detected\"}}")); + assert(!everlive_ctl_response_has_error( + "{\"result\":{\"status\":\"error\"}}")); + assert(!everlive_ctl_response_has_error("{\"result\":{}}")); + assert(!everlive_ctl_response_has_error(NULL)); + TEST_PASS("Only an \"error\" key is treated as a failure"); +} + +static void test_command_payloads(void) +{ + char params[256]; + char req[512]; + double ev; + double percent; + unsigned long rgb; + + printf("Testing numeric command payloads...\n"); + + /* hdr-ev */ + assert(everlive_ctl_parse_double("-0.5", &ev) == 0); + assert(everlive_ctl_format_exposure(ev, params, sizeof(params)) == 0); + assert(strcmp(params, "{\"exposure\":-0.5}") == 0); + assert(everlive_ctl_build_request("hdr.exposure", params, req, + sizeof(req)) == 0); + assert(strstr(req, "\"exposure\":-0.5") != NULL); + TEST_PASS("hdr-ev payload uses the parsed value"); + + /* volume: 85% -> 0.85 gain */ + assert(everlive_ctl_parse_double("85", &percent) == 0); + assert(everlive_ctl_format_volume(percent, params, + sizeof(params)) == 0); + assert(strcmp(params, "{\"gain\":0.85}") == 0); + assert(everlive_ctl_format_volume(150.0, params, sizeof(params)) == 0); + assert(everlive_ctl_format_volume(-1.0, params, sizeof(params)) == -1); + assert(everlive_ctl_format_volume(151.0, params, sizeof(params)) == -1); + TEST_PASS("volume gain is scaled and range-checked"); + + /* led: FF8020 -> 255,128,32 */ + assert(everlive_ctl_parse_hex_rgb("FF8020", &rgb) == 0); + assert(everlive_ctl_format_led(rgb, params, sizeof(params)) == 0); + assert(strcmp(params, "{\"r\":255,\"g\":128,\"b\":32}") == 0); + TEST_PASS("led payload decomposes RRGGBB correctly"); +} + +int main(void) +{ + printf("==================================================\n"); + printf(" everlive-ctl Protocol & Validation Tests \n"); + printf("==================================================\n"); + + test_build_request(); + test_parse_double(); + test_parse_hex_rgb(); + test_json_escape(); + test_response_has_error(); + test_command_payloads(); + + printf("==================================================\n"); + printf(" ALL EVERLIVE-CTL PROTOCOL TESTS PASSED!\n"); + printf("==================================================\n"); + return 0; +} diff --git a/tests/test_frame_convert.c b/tests/test_frame_convert.c new file mode 100644 index 0000000..f2fcbd5 --- /dev/null +++ b/tests/test_frame_convert.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include "everlive_frame.h" + +static void test_yuy2_to_rgb_pure_colors(void) +{ + printf("[*] Testing pure color YUY2 -> RGB conversions...\n"); + + /* Test Black: Y=16 (or 0), U=128, V=128 */ + uint8_t black_yuy2[4] = { 0, 128, 0, 128 }; + uint8_t black_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(black_yuy2, black_rgb, 2, 1); + assert(black_rgb[0] == 0 && black_rgb[1] == 0 && black_rgb[2] == 0); + assert(black_rgb[3] == 0 && black_rgb[4] == 0 && black_rgb[5] == 0); + + /* Test White: Y=255, U=128, V=128 */ + uint8_t white_yuy2[4] = { 255, 128, 255, 128 }; + uint8_t white_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(white_yuy2, white_rgb, 2, 1); + assert(white_rgb[0] == 255 && white_rgb[1] == 255 && white_rgb[2] == 255); + assert(white_rgb[3] == 255 && white_rgb[4] == 255 && white_rgb[5] == 255); + + /* Test Red: Y=81, U=90, V=240 */ + uint8_t red_yuy2[4] = { 81, 90, 81, 240 }; + uint8_t red_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(red_yuy2, red_rgb, 2, 1); + /* Red component should be dominant */ + assert(red_rgb[0] > 200); + assert(red_rgb[1] < 100); + assert(red_rgb[2] < 100); + + printf(" Pure colors OK.\n"); +} + +static void test_yuy2_to_rgb_scaled(void) +{ + printf("[*] Testing YUY2 -> RGB downscaling...\n"); + + int src_w = 1920; + int src_h = 1080; + int dst_w = 960; + int dst_h = 540; + + uint8_t *src = malloc(src_w * src_h * 2); + uint8_t *dst = malloc(dst_w * dst_h * 3); + assert(src && dst); + + /* Fill with alternating pattern */ + for (int i = 0; i < src_w * src_h * 2; i += 4) { + src[i + 0] = 128; /* Y0 */ + src[i + 1] = 64; /* U */ + src[i + 2] = 200; /* Y1 */ + src[i + 3] = 192; /* V */ + } + + everlive_yuy2_to_rgb_scaled(src, dst, src_w, src_h, dst_w, dst_h); + + /* Verify non-zero scaled output and bounds */ + bool non_zero = false; + for (int i = 0; i < dst_w * dst_h * 3; i++) { + if (dst[i] > 0) + non_zero = true; + } + assert(non_zero); + + free(src); + free(dst); + printf(" Scaled conversion OK.\n"); +} + +static void test_invalid_parameters(void) +{ + printf("[*] Testing edge cases & NULL robustness...\n"); + uint8_t buf[16] = { 0 }; + + /* NULL pointers must not crash */ + everlive_yuy2_to_rgb(NULL, buf, 2, 1); + everlive_yuy2_to_rgb(buf, NULL, 2, 1); + everlive_yuy2_to_rgb(buf, buf, 0, 1); + everlive_yuy2_to_rgb(buf, buf, 2, 0); + + everlive_yuy2_to_rgb_scaled(NULL, buf, 4, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, NULL, 4, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 0, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 0, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 0, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 2, 0); + + /* Odd width is not a valid YUY2 layout; the output must stay untouched. */ + uint8_t odd_src[8] = { 0 }; + uint8_t odd_dst[12]; + memset(odd_dst, 0xAA, sizeof(odd_dst)); + everlive_yuy2_to_rgb(odd_src, odd_dst, 3, 1); + for (size_t i = 0; i < sizeof(odd_dst); i++) + assert(odd_dst[i] == 0xAA); + + memset(odd_dst, 0x55, sizeof(odd_dst)); + everlive_yuy2_to_rgb_scaled(odd_src, odd_dst, 3, 1, 2, 2); + for (size_t i = 0; i < sizeof(odd_dst); i++) + assert(odd_dst[i] == 0x55); + + printf(" Edge cases OK.\n"); +} + +int main(void) +{ + printf("=== Running Frame Converter Unit Tests ===\n"); + test_yuy2_to_rgb_pure_colors(); + test_yuy2_to_rgb_scaled(); + test_invalid_parameters(); + printf("=== All Frame Converter Tests Passed! ===\n"); + return 0; +} diff --git a/tools/opengc573-userd/test_framegen.c b/tests/test_framegen.c similarity index 100% rename from tools/opengc573-userd/test_framegen.c rename to tests/test_framegen.c diff --git a/tests/test_ipc_json.c b/tests/test_ipc_json.c deleted file mode 100644 index fdcf9e9..0000000 --- a/tests/test_ipc_json.c +++ /dev/null @@ -1,245 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-only */ -/* - * test_ipc_json.c — Unit tests for GC573 Hand-rolled JSON Scanner and RPC Formatter - */ - -#include -#include -#include -#include -#include - -#include "../tools/opengc573-userd/gc573-user-compat.h" -#include "../tools/opengc573-userd/gc573_user_ipc.c" - -static int g_failures = 0; - -#define TEST_ASSERT(cond, msg) do { \ - if (!(cond)) { \ - fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); \ - g_failures++; \ - } else { \ - printf(" [PASS] %s\n", msg); \ - } \ -} while (0) - -static void test_json_get_string(void) -{ - char out[64]; - const char *json = "{\"jsonrpc\":\"2.0\",\"method\":\"led.set\",\"params\":{\"mode\":\"wave\"}}"; - const char *json_dup_val = "{\"title\":\"mode\",\"mode\":\"wave\"}"; - const char *json_param_method = "{\"jsonrpc\":\"2.0\",\"params\":{\"text\":\"method\"},\"method\":\"led.set\"}"; - - printf("Testing json_get_string...\n"); - TEST_ASSERT(json_get_string(json, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, - "Extracts top-level string 'method'"); - TEST_ASSERT(json_get_string(json, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, - "Extracts nested string 'mode'"); - TEST_ASSERT(json_get_string(json_dup_val, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, - "Distinguishes object key from prior string value matching key name"); - TEST_ASSERT(json_get_string(json_param_method, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, - "Extracts top-level method even if params contains identical string value"); - TEST_ASSERT(!json_get_string(json, "missing", out, sizeof(out)), - "Fails cleanly on missing key"); -} - -static void test_json_get_long(void) -{ - long val = 0; - const char *json = "{\"id\":42,\"code\":-32700,\"zero\":0}"; - - printf("Testing json_get_long...\n"); - TEST_ASSERT(json_get_long(json, "id", &val) && val == 42, - "Extracts positive integer 'id': 42"); - TEST_ASSERT(json_get_long(json, "code", &val) && val == -32700, - "Extracts negative integer 'code': -32700"); - TEST_ASSERT(json_get_long(json, "zero", &val) && val == 0, - "Extracts zero integer: 0"); - TEST_ASSERT(!json_get_long(json, "nonexistent", &val), - "Fails cleanly on non-existent key"); -} - -static void test_json_get_bool(void) -{ - bool val = false; - const char *json = "{\"active\":true,\"disabled\":false,\"num\":1}"; - - printf("Testing json_get_bool...\n"); - TEST_ASSERT(json_get_bool(json, "active", &val) && val == true, - "Extracts boolean true"); - TEST_ASSERT(json_get_bool(json, "disabled", &val) && val == false, - "Extracts boolean false"); - TEST_ASSERT(!json_get_bool(json, "num", &val), - "Rejects non-boolean value"); -} - -static void test_json_get_double(void) -{ - double val = 0.0; - const char *json = "{\"exposure\":-1.5,\"gain\":0.85}"; - - printf("Testing json_get_double...\n"); - TEST_ASSERT(json_get_double(json, "exposure", &val) && val == -1.5, - "Extracts negative float 'exposure': -1.5"); - TEST_ASSERT(json_get_double(json, "gain", &val) && val > 0.84 && val < 0.86, - "Extracts positive float 'gain': 0.85"); - TEST_ASSERT(!json_get_double(json, "nonexistent", &val), - "Fails cleanly on missing float key"); -} - -static void test_json_escape_into(void) -{ - char dst[128]; - - printf("Testing json_escape_into...\n"); - json_escape_into(dst, sizeof(dst), "Simple string"); - TEST_ASSERT(strcmp(dst, "Simple string") == 0, "Leaves plain ASCII unchanged"); - - json_escape_into(dst, sizeof(dst), "Hello \"World\"\\Test\nNew\tTab"); - TEST_ASSERT(strcmp(dst, "Hello \\\"World\\\"\\\\Test\\u000aNew\\u0009Tab") == 0, - "Escapes quotes, backslashes, and control characters"); -} - -static void test_json_format_id(void) -{ - char dst[64]; - - printf("Testing json_format_id...\n"); - json_format_id(dst, sizeof(dst), NULL); - TEST_ASSERT(strcmp(dst, "null") == 0, "Formats NULL id as null"); - - json_format_id(dst, sizeof(dst), "null"); - TEST_ASSERT(strcmp(dst, "null") == 0, "Formats 'null' string as null"); - - json_format_id(dst, sizeof(dst), "1234"); - TEST_ASSERT(strcmp(dst, "1234") == 0, "Preserves numeric integer id 1234"); - - json_format_id(dst, sizeof(dst), "-1"); - TEST_ASSERT(strcmp(dst, "-1") == 0, "Preserves negative integer id -1"); - - json_format_id(dst, sizeof(dst), "\"client-req-99\""); - TEST_ASSERT(strcmp(dst, "\"client-req-99\"") == 0, "Preserves and sanitizes quoted string id"); - - json_format_id(dst, sizeof(dst), "raw-id"); - TEST_ASSERT(strcmp(dst, "\"raw-id\"") == 0, "Encapsulates unquoted string id"); - - json_format_id(dst, sizeof(dst), "\"injection\n\""); - TEST_ASSERT(strstr(dst, "\\u000a") != NULL, "Re-escapes dangerous newline injection in string id"); -} - -static void test_json_find_value(void) -{ - char out[64]; - const char *json_num = "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"ping\"}"; - const char *json_str = "{\"jsonrpc\":\"2.0\",\"id\":\"req-abc\",\"method\":\"status.get\"}"; - const char *json_null = "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"led.set\"}"; - - printf("Testing json_find_value...\n"); - TEST_ASSERT(json_find_value(json_num, "method", out, sizeof(out)) && strcmp(out, "ping") == 0, - "Extracts method string from numeric ID request"); - TEST_ASSERT(json_find_value(json_num, "id", out, sizeof(out)) && strcmp(out, "42") == 0, - "Extracts numeric ID: 42"); - - TEST_ASSERT(json_find_value(json_str, "method", out, sizeof(out)) && strcmp(out, "status.get") == 0, - "Extracts method string from string ID request"); - TEST_ASSERT(json_find_value(json_str, "id", out, sizeof(out)) && strcmp(out, "req-abc") == 0, - "Extracts string ID: req-abc"); - - TEST_ASSERT(json_find_value(json_null, "id", out, sizeof(out)) && strcmp(out, "null") == 0, - "Extracts null ID: null"); - TEST_ASSERT(!json_find_value(json_num, "missing", out, sizeof(out)), - "Fails cleanly on missing key in json_find_value"); -} - -static void test_json_negative_edge_cases(void) -{ - char out[64]; - long lval = 0; - double dval = 0.0; - bool bval = false; - const char *valid_json = "{\"str\":\"hello\",\"num\":123,\"flag\":true,\"flt\":3.14}"; - - printf("Testing JSON negative edge cases...\n"); - - /* NULL pointer handling */ - TEST_ASSERT(!json_get_string(NULL, "str", out, sizeof(out)), "json_get_string rejects NULL json"); - TEST_ASSERT(!json_get_string(valid_json, NULL, out, sizeof(out)), "json_get_string rejects NULL key"); - TEST_ASSERT(!json_get_string(valid_json, "str", NULL, sizeof(out)), "json_get_string rejects NULL output"); - TEST_ASSERT(!json_get_string(valid_json, "str", out, 0), "json_get_string rejects 0 outsz"); - - TEST_ASSERT(!json_get_long(NULL, "num", &lval), "json_get_long rejects NULL json"); - TEST_ASSERT(!json_get_long(valid_json, NULL, &lval), "json_get_long rejects NULL key"); - TEST_ASSERT(!json_get_long(valid_json, "num", NULL), "json_get_long rejects NULL output"); - - TEST_ASSERT(!json_get_double(NULL, "flt", &dval), "json_get_double rejects NULL json"); - TEST_ASSERT(!json_get_double(valid_json, NULL, &dval), "json_get_double rejects NULL key"); - TEST_ASSERT(!json_get_double(valid_json, "flt", NULL), "json_get_double rejects NULL output"); - - TEST_ASSERT(!json_get_bool(NULL, "flag", &bval), "json_get_bool rejects NULL json"); - TEST_ASSERT(!json_get_bool(valid_json, NULL, &bval), "json_get_bool rejects NULL key"); - TEST_ASSERT(!json_get_bool(valid_json, "flag", NULL), "json_get_bool rejects NULL output"); - - TEST_ASSERT(!json_find_value(NULL, "id", out, sizeof(out)), "json_find_value rejects NULL json"); - TEST_ASSERT(!json_find_value(valid_json, NULL, out, sizeof(out)), "json_find_value rejects NULL key"); - TEST_ASSERT(!json_find_value(valid_json, "id", NULL, sizeof(out)), "json_find_value rejects NULL output"); - TEST_ASSERT(!json_find_value(valid_json, "id", out, 0), "json_find_value rejects 0 outsz"); - - /* Malformed JSON input */ - TEST_ASSERT(!json_get_string("", "str", out, sizeof(out)), "Rejects empty JSON string"); - TEST_ASSERT(!json_get_string("not json at all", "str", out, sizeof(out)), "Rejects non-JSON string"); - TEST_ASSERT(!json_get_string("{\"unclosed\":", "unclosed", out, sizeof(out)), "Rejects unclosed JSON"); - TEST_ASSERT(!json_get_string("[1, 2, 3]", "str", out, sizeof(out)), "Rejects array root JSON"); - TEST_ASSERT(!json_get_string("{unquoted_key: 1}", "unquoted_key", out, sizeof(out)), "Rejects unquoted keys"); - - /* Type mismatches */ - TEST_ASSERT(!json_get_string(valid_json, "num", out, sizeof(out)), - "Type mismatch: json_get_string rejects primitive number"); - TEST_ASSERT(!json_get_long(valid_json, "str", &lval), - "Type mismatch: json_get_long rejects string value"); - TEST_ASSERT(!json_get_double(valid_json, "str", &dval), - "Type mismatch: json_get_double rejects string value"); - TEST_ASSERT(!json_get_bool(valid_json, "str", &bval), - "Type mismatch: json_get_bool rejects string value"); - TEST_ASSERT(!json_get_bool(valid_json, "num", &bval), - "Type mismatch: json_get_bool rejects number value"); - - /* Destination buffer too small for string */ - char small_buf[4]; - TEST_ASSERT(!json_get_string(valid_json, "str", small_buf, sizeof(small_buf)), - "Safely rejects string when destination buffer is too small"); - - /* json_escape_into negative edge cases */ - json_escape_into(NULL, 10, "test"); /* Should not crash */ - json_escape_into(out, 0, "test"); /* Should not crash */ - json_escape_into(out, sizeof(out), NULL); - TEST_ASSERT(out[0] == '\0', "json_escape_into handles NULL src cleanly"); - - /* json_format_id negative edge cases */ - json_format_id(NULL, 10, "id"); /* Should not crash */ - json_format_id(out, 0, "id"); /* Should not crash */ -} - -int main(void) -{ - printf("==================================================\n"); - printf(" GC573 IPC JSON Scanner Test Suite\n"); - printf("==================================================\n"); - - test_json_get_string(); - test_json_get_long(); - test_json_get_double(); - test_json_get_bool(); - test_json_escape_into(); - test_json_format_id(); - test_json_find_value(); - test_json_negative_edge_cases(); - - printf("==================================================\n"); - if (g_failures == 0) { - printf(" ALL GC573 IPC JSON TESTS PASSED!\n"); - return 0; - } else { - printf(" %d TESTS FAILED!\n", g_failures); - return 1; - } -} diff --git a/tests/test_json_rpc.c b/tests/test_json_rpc.c new file mode 100644 index 0000000..a22c83c --- /dev/null +++ b/tests/test_json_rpc.c @@ -0,0 +1,248 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_json_rpc.c — Unit tests for the libeverlive-core JSON scanners and RPC formatters + */ + +#include +#include +#include +#include +#include + +#include "everlive_json.h" + +static int g_failures = 0; + +#define TEST_ASSERT(cond, msg) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); \ + g_failures++; \ + } else { \ + printf(" [PASS] %s\n", msg); \ + } \ +} while (0) + +static void test_json_get_string(void) +{ + char out[64]; + const char *json = "{\"jsonrpc\":\"2.0\",\"method\":\"led.set\",\"params\":{\"mode\":\"wave\"}}"; + const char *json_dup_val = "{\"title\":\"mode\",\"mode\":\"wave\"}"; + const char *json_param_method = "{\"jsonrpc\":\"2.0\",\"params\":{\"text\":\"method\"},\"method\":\"led.set\"}"; + + printf("Testing everlive_json_get_string...\n"); + TEST_ASSERT(everlive_json_get_string(json, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + "Extracts top-level string 'method'"); + TEST_ASSERT(everlive_json_get_string(json, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + "Extracts nested string 'mode'"); + TEST_ASSERT(everlive_json_get_string(json_dup_val, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + "Distinguishes object key from prior string value matching key name"); + TEST_ASSERT(everlive_json_get_string(json_param_method, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + "Extracts top-level method even if params contains identical string value"); + TEST_ASSERT(!everlive_json_get_string(json, "missing", out, sizeof(out)), + "Fails cleanly on missing key"); +} + +static void test_json_get_long(void) +{ + long val = 0; + const char *json = "{\"id\":42,\"code\":-32700,\"zero\":0}"; + + printf("Testing everlive_json_get_long...\n"); + TEST_ASSERT(everlive_json_get_long(json, "id", &val) && val == 42, + "Extracts positive integer 'id': 42"); + TEST_ASSERT(everlive_json_get_long(json, "code", &val) && val == -32700, + "Extracts negative integer 'code': -32700"); + TEST_ASSERT(everlive_json_get_long(json, "zero", &val) && val == 0, + "Extracts zero integer: 0"); + TEST_ASSERT(!everlive_json_get_long(json, "nonexistent", &val), + "Fails cleanly on non-existent key"); +} + +static void test_json_get_bool(void) +{ + bool val = false; + const char *json = "{\"active\":true,\"disabled\":false,\"num\":1}"; + + printf("Testing everlive_json_get_bool...\n"); + TEST_ASSERT(everlive_json_get_bool(json, "active", &val) && val == true, + "Extracts boolean true"); + TEST_ASSERT(everlive_json_get_bool(json, "disabled", &val) && val == false, + "Extracts boolean false"); + TEST_ASSERT(!everlive_json_get_bool(json, "num", &val), + "Rejects non-boolean value"); +} + +static void test_json_get_double(void) +{ + double val = 0.0; + const char *json = "{\"exposure\":-1.5,\"gain\":0.85}"; + + printf("Testing everlive_json_get_double...\n"); + TEST_ASSERT(everlive_json_get_double(json, "exposure", &val) && val == -1.5, + "Extracts negative float 'exposure': -1.5"); + TEST_ASSERT(everlive_json_get_double(json, "gain", &val) && val > 0.84 && val < 0.86, + "Extracts positive float 'gain': 0.85"); + TEST_ASSERT(!everlive_json_get_double(json, "nonexistent", &val), + "Fails cleanly on missing float key"); + TEST_ASSERT(!everlive_json_get_double("{\"val\":NaN}", "val", &val), + "Rejects NaN in double field"); + TEST_ASSERT(!everlive_json_get_double("{\"val\":Infinity}", "val", &val), + "Rejects Infinity in double field"); +} + +static void test_json_escape_into(void) +{ + char dst[128]; + + printf("Testing everlive_json_escape_into...\n"); + everlive_json_escape_into(dst, sizeof(dst), "Simple string"); + TEST_ASSERT(strcmp(dst, "Simple string") == 0, "Leaves plain ASCII unchanged"); + + everlive_json_escape_into(dst, sizeof(dst), "Hello \"World\"\\Test\nNew\tTab"); + TEST_ASSERT(strcmp(dst, "Hello \\\"World\\\"\\\\Test\\nNew\\tTab") == 0, + "Escapes quotes, backslashes, and control characters"); +} + +static void test_json_format_id(void) +{ + char dst[64]; + + printf("Testing everlive_json_format_id...\n"); + everlive_json_format_id(dst, sizeof(dst), NULL); + TEST_ASSERT(strcmp(dst, "null") == 0, "Formats NULL id as null"); + + everlive_json_format_id(dst, sizeof(dst), "null"); + TEST_ASSERT(strcmp(dst, "null") == 0, "Formats 'null' string as null"); + + everlive_json_format_id(dst, sizeof(dst), "1234"); + TEST_ASSERT(strcmp(dst, "1234") == 0, "Preserves numeric integer id 1234"); + + everlive_json_format_id(dst, sizeof(dst), "-1"); + TEST_ASSERT(strcmp(dst, "-1") == 0, "Preserves negative integer id -1"); + + everlive_json_format_id(dst, sizeof(dst), "\"client-req-99\""); + TEST_ASSERT(strcmp(dst, "\"client-req-99\"") == 0, "Preserves and sanitizes quoted string id"); + + everlive_json_format_id(dst, sizeof(dst), "raw-id"); + TEST_ASSERT(strcmp(dst, "\"raw-id\"") == 0, "Encapsulates unquoted string id"); + + everlive_json_format_id(dst, sizeof(dst), "\"injection\n\""); + TEST_ASSERT(strstr(dst, "\\n") != NULL, "Re-escapes dangerous newline injection in string id"); +} + +static void test_json_find_value(void) +{ + char out[64]; + const char *json_num = "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"ping\"}"; + const char *json_str = "{\"jsonrpc\":\"2.0\",\"id\":\"req-abc\",\"method\":\"status.get\"}"; + const char *json_null = "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"led.set\"}"; + + printf("Testing everlive_json_find_value...\n"); + TEST_ASSERT(everlive_json_find_value(json_num, "method", out, sizeof(out)) && strcmp(out, "ping") == 0, + "Extracts method string from numeric ID request"); + TEST_ASSERT(everlive_json_find_value(json_num, "id", out, sizeof(out)) && strcmp(out, "42") == 0, + "Extracts numeric ID: 42"); + + TEST_ASSERT(everlive_json_find_value(json_str, "method", out, sizeof(out)) && strcmp(out, "status.get") == 0, + "Extracts method string from string ID request"); + TEST_ASSERT(everlive_json_find_value(json_str, "id", out, sizeof(out)) && strcmp(out, "req-abc") == 0, + "Extracts string ID: req-abc"); + + TEST_ASSERT(everlive_json_find_value(json_null, "id", out, sizeof(out)) && strcmp(out, "null") == 0, + "Extracts null ID: null"); + TEST_ASSERT(!everlive_json_find_value(json_num, "missing", out, sizeof(out)), + "Fails cleanly on missing key in everlive_json_find_value"); +} + +static void test_json_negative_edge_cases(void) +{ + char out[64]; + long lval = 0; + double dval = 0.0; + bool bval = false; + const char *valid_json = "{\"str\":\"hello\",\"num\":123,\"flag\":true,\"flt\":3.14}"; + + printf("Testing JSON negative edge cases...\n"); + + /* NULL pointer handling */ + TEST_ASSERT(!everlive_json_get_string(NULL, "str", out, sizeof(out)), "everlive_json_get_string rejects NULL json"); + TEST_ASSERT(!everlive_json_get_string(valid_json, NULL, out, sizeof(out)), "everlive_json_get_string rejects NULL key"); + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", NULL, sizeof(out)), "everlive_json_get_string rejects NULL output"); + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", out, 0), "everlive_json_get_string rejects 0 outsz"); + + TEST_ASSERT(!everlive_json_get_long(NULL, "num", &lval), "everlive_json_get_long rejects NULL json"); + TEST_ASSERT(!everlive_json_get_long(valid_json, NULL, &lval), "everlive_json_get_long rejects NULL key"); + TEST_ASSERT(!everlive_json_get_long(valid_json, "num", NULL), "everlive_json_get_long rejects NULL output"); + + TEST_ASSERT(!everlive_json_get_double(NULL, "flt", &dval), "everlive_json_get_double rejects NULL json"); + TEST_ASSERT(!everlive_json_get_double(valid_json, NULL, &dval), "everlive_json_get_double rejects NULL key"); + TEST_ASSERT(!everlive_json_get_double(valid_json, "flt", NULL), "everlive_json_get_double rejects NULL output"); + + TEST_ASSERT(!everlive_json_get_bool(NULL, "flag", &bval), "everlive_json_get_bool rejects NULL json"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, NULL, &bval), "everlive_json_get_bool rejects NULL key"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "flag", NULL), "everlive_json_get_bool rejects NULL output"); + + TEST_ASSERT(!everlive_json_find_value(NULL, "id", out, sizeof(out)), "everlive_json_find_value rejects NULL json"); + TEST_ASSERT(!everlive_json_find_value(valid_json, NULL, out, sizeof(out)), "everlive_json_find_value rejects NULL key"); + TEST_ASSERT(!everlive_json_find_value(valid_json, "id", NULL, sizeof(out)), "everlive_json_find_value rejects NULL output"); + TEST_ASSERT(!everlive_json_find_value(valid_json, "id", out, 0), "everlive_json_find_value rejects 0 outsz"); + + /* Malformed JSON input */ + TEST_ASSERT(!everlive_json_get_string("", "str", out, sizeof(out)), "Rejects empty JSON string"); + TEST_ASSERT(!everlive_json_get_string("not json at all", "str", out, sizeof(out)), "Rejects non-JSON string"); + TEST_ASSERT(!everlive_json_get_string("{\"unclosed\":", "unclosed", out, sizeof(out)), "Rejects unclosed JSON"); + TEST_ASSERT(!everlive_json_get_string("[1, 2, 3]", "str", out, sizeof(out)), "Rejects array root JSON"); + TEST_ASSERT(!everlive_json_get_string("{unquoted_key: 1}", "unquoted_key", out, sizeof(out)), "Rejects unquoted keys"); + + /* Type mismatches */ + TEST_ASSERT(!everlive_json_get_string(valid_json, "num", out, sizeof(out)), + "Type mismatch: everlive_json_get_string rejects primitive number"); + TEST_ASSERT(!everlive_json_get_long(valid_json, "str", &lval), + "Type mismatch: everlive_json_get_long rejects string value"); + TEST_ASSERT(!everlive_json_get_double(valid_json, "str", &dval), + "Type mismatch: everlive_json_get_double rejects string value"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "str", &bval), + "Type mismatch: everlive_json_get_bool rejects string value"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "num", &bval), + "Type mismatch: everlive_json_get_bool rejects number value"); + + /* Destination buffer too small for string */ + char small_buf[4]; + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", small_buf, sizeof(small_buf)), + "Safely rejects string when destination buffer is too small"); + + /* everlive_json_escape_into negative edge cases */ + everlive_json_escape_into(NULL, 10, "test"); /* Should not crash */ + everlive_json_escape_into(out, 0, "test"); /* Should not crash */ + everlive_json_escape_into(out, sizeof(out), NULL); + TEST_ASSERT(out[0] == '\0', "everlive_json_escape_into handles NULL src cleanly"); + + /* everlive_json_format_id negative edge cases */ + everlive_json_format_id(NULL, 10, "id"); /* Should not crash */ + everlive_json_format_id(out, 0, "id"); /* Should not crash */ +} + +int main(void) +{ + printf("==================================================\n"); + printf(" libeverlive-core JSON-RPC Scanner Tests \n"); + printf("==================================================\n"); + + test_json_get_string(); + test_json_get_long(); + test_json_get_double(); + test_json_get_bool(); + test_json_escape_into(); + test_json_format_id(); + test_json_find_value(); + test_json_negative_edge_cases(); + + printf("==================================================\n"); + if (g_failures == 0) { + printf(" ALL JSON-RPC SCANNER TESTS PASSED!\n"); + return 0; + } else { + printf(" %d TESTS FAILED!\n", g_failures); + return 1; + } +} diff --git a/tools/everlive/everlive_viewer.py b/tools/everlive/everlive_viewer.py index 517ed88..79fc6b8 100755 --- a/tools/everlive/everlive_viewer.py +++ b/tools/everlive/everlive_viewer.py @@ -1551,6 +1551,13 @@ def ctl(self, *args: str) -> Optional[str]: except Exception: return None + def _duo_ipc(self): + """Return the attached control client, or None when unavailable.""" + ipc = getattr(self, "ipc", None) + if ipc is not None and getattr(ipc, "connected", False): + return ipc + return None + def set_duo_input(self, port: int) -> bool: """Select HDMI 1 (0) or HDMI 2 (1) on the Live Gamer DUO.""" port = 1 if int(port) else 0 @@ -1560,6 +1567,9 @@ def set_duo_input(self, port: int) -> bool: return False if not self.is_running: return True + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("input.set", {"port": port + 1}) return self.ctl("input", str(port + 1)) is not None def apply_duo_led(self, mode: int) -> bool: @@ -1573,17 +1583,27 @@ def apply_duo_led(self, mode: int) -> bool: effect = "breathing" else: effect = "static" + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("led.mode.set", {"mode": effect}) return self.ctl("led", effect) is not None def duo_hotplug(self, port: int = 0) -> bool: """Pulse HPD on IT68051 (HDMI 1) or IT6802 (HDMI 2).""" node = "it68051_hpd" if int(port) == 0 else "it6802_hpd" + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("hotplug", {"port": 1 if int(port) == 0 else 2}) return self.ctl("action", node, "1") is not None def duo_edid_policy(self, bridge: bool) -> bool: """Select internal bridge EDID (True) or downstream copy (False).""" node = ("splitter_windows_bridge_edid" if bridge else "splitter_windows_edid_copy") + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("edid.set", + {"profile": "default" if bridge else "copy"}) return self.ctl("action", node, "1") is not None def _start_vfio_duo(self, fullscreen: bool, vsync: bool, @@ -2217,6 +2237,10 @@ def __init__(self, streamer: GameStreamer, vsync: bool = True, streamer_mode: bo # QSocketNotifier keeps this on the GUI thread: no worker threads # and no cross-thread signals. self._ipc = EverLiveIpcClient(self) + if self.streamer is not None: + # Let the engine wrapper prefer IPC for DUO input/HPD/EDID + # when the hub is attached; gc570d-ctl stays as the fallback. + self.streamer.ipc = self._ipc self._ipc.connectedChanged.connect(self._on_remote_connection) self._ipc.statusReceived.connect(self._on_remote_status) self._ipc.errorRaised.connect(lambda msg: print(f"[IPC] {msg}")) @@ -3631,7 +3655,7 @@ def _vfio_preflight(self) -> bool: # ----------------------------------------------------------------- def _ipc_try_attach(self): """Attach to a running streamer (local or external).""" - if self.duo_mode or self._ipc is None or self._ipc_shutdown: + if self._ipc is None or self._ipc_shutdown: return if self._ipc.connected: return @@ -3885,6 +3909,13 @@ def _send_ipc_led(self, mode: int) -> bool: """ if self._ipc is None or not self._ipc.connected: return False + if self.duo_mode: + # The DUO's animations are named differently from the GC573 + # renderer's: mode 8 is its colorful cycle, mode 9 breathing. + if mode == 8: + return self._ipc.set_led_mode("colorful") + if mode == 9: + return self._ipc.set_led_mode("breathing") if mode == 8: return self._ipc.set_led_mode("wave") if mode == 9: diff --git a/tools/libeverlive-core/Makefile b/tools/libeverlive-core/Makefile new file mode 100644 index 0000000..68ec76f --- /dev/null +++ b/tools/libeverlive-core/Makefile @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EverLive core library: card registry, frame conversion, JSON-RPC helpers, +# control-socket resolution, and the everlive-ctl CLI. + +CC ?= gcc +AR ?= ar +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -std=gnu11 -D_GNU_SOURCE -fPIC +DEPFLAGS := -MMD -MP + +SOVERSION := 0 +VERSION := 0.2.0 +SONAME := libeverlive_core.so.$(SOVERSION) +SHARED_REAL := libeverlive_core.so.$(VERSION) + +TARGET_STATIC := libeverlive_core.a +TARGET_SHARED := libeverlive_core.so +TARGET_CLI := everlive-ctl + +CORE_OBJS := everlive_card.o everlive_frame.o everlive_json.o \ + everlive_ipc.o everlive_ctl_proto.o third_party/jsmn.o +CTL_OBJS := everlive_ctl.o + +INCLUDES := -I. + +PREFIX ?= /usr/local +DESTDIR ?= +BINDIR ?= $(PREFIX)/bin +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +SYSTEMD_USER_DIR ?= $(PREFIX)/lib/systemd/user + +all: $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) + +$(TARGET_STATIC): $(CORE_OBJS) + $(AR) rcs $@ $^ + +$(SHARED_REAL): $(CORE_OBJS) + $(CC) -shared -Wl,-soname,$(SONAME) -o $@ $^ + +$(SONAME): $(SHARED_REAL) + ln -sf $(SHARED_REAL) $@ + +$(TARGET_SHARED): $(SONAME) + ln -sf $(SONAME) $@ + +$(TARGET_CLI): $(CTL_OBJS) $(TARGET_STATIC) + $(CC) $(CFLAGS) -o $@ $(CTL_OBJS) $(TARGET_STATIC) + +third_party/jsmn.o: third_party/jsmn.c third_party/jsmn.h + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -c -o $@ $< + +%.o: %.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -c -o $@ $< + +-include $(CORE_OBJS:.o=.d) $(CTL_OBJS:.o=.d) + +test selftest: all + $(MAKE) -C ../../tests test + +install: all + install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBDIR) \ + $(DESTDIR)$(INCLUDEDIR) $(DESTDIR)$(SYSTEMD_USER_DIR) + install -m 0755 $(TARGET_CLI) $(DESTDIR)$(BINDIR)/$(TARGET_CLI) + install -m 0755 everlive-service.sh $(DESTDIR)$(BINDIR)/everlive-service + install -m 0644 $(SHARED_REAL) $(DESTDIR)$(LIBDIR)/$(SHARED_REAL) + ln -sf $(SHARED_REAL) $(DESTDIR)$(LIBDIR)/$(SONAME) + ln -sf $(SONAME) $(DESTDIR)$(LIBDIR)/$(TARGET_SHARED) + install -m 0644 $(TARGET_STATIC) $(DESTDIR)$(LIBDIR)/$(TARGET_STATIC) + install -m 0644 everlive_card.h everlive_frame.h everlive_ctl.h \ + everlive_json.h everlive_ipc.h $(DESTDIR)$(INCLUDEDIR)/ + sed 's|/usr/local|$(PREFIX)|g' everlive.service \ + > $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + chmod 0644 $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/$(TARGET_CLI) \ + $(DESTDIR)$(BINDIR)/everlive-service \ + $(DESTDIR)$(LIBDIR)/$(SHARED_REAL) \ + $(DESTDIR)$(LIBDIR)/$(SONAME) \ + $(DESTDIR)$(LIBDIR)/$(TARGET_SHARED) \ + $(DESTDIR)$(LIBDIR)/$(TARGET_STATIC) \ + $(DESTDIR)$(INCLUDEDIR)/everlive_card.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_frame.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_ctl.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_json.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_ipc.h \ + $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + +clean: + rm -f $(CORE_OBJS) $(CTL_OBJS) $(CORE_OBJS:.o=.d) $(CTL_OBJS:.o=.d) \ + $(TARGET_STATIC) $(TARGET_SHARED) $(SONAME) $(SHARED_REAL) \ + $(TARGET_CLI) + +.PHONY: all test selftest install uninstall clean diff --git a/tools/libeverlive-core/everlive-service.sh b/tools/libeverlive-core/everlive-service.sh new file mode 100755 index 0000000..d0357f1 --- /dev/null +++ b/tools/libeverlive-core/everlive-service.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-only +# ============================================================================== +# everlive-service.sh — Universal Background Capture Service for EverLive +# +# Automatically detects the installed card (GC570D, GC573), ensures vfio-pci +# binding, raises DMA memlock limits, and runs the capture engine in headless mode. +# Keeps HDMI passthrough, PipeWire audio, and virtual cameras active at all times. +# ============================================================================== +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +CTL_BIN="${SCRIPT_DIR}/everlive-ctl" +if [ ! -x "${CTL_BIN}" ]; then + CTL_BIN="$(command -v everlive-ctl 2>/dev/null || true)" +fi + +CARD_MODEL="" +CARD_PCI="" +IS_BOUND=0 + +# 1. Detect the installed card. Prefer the core registry (everlive-ctl), +# then fall back to sysfs when the CLI is unavailable. +detect_card_sysfs() { + local dev vendor device subsystem + + CARD_MODEL="" + CARD_PCI="" + for dev in /sys/bus/pci/devices/*; do + [ -f "$dev/vendor" ] || continue + [ -f "$dev/device" ] || continue + vendor=$(cat "$dev/vendor" 2>/dev/null || true) + device=$(cat "$dev/device" 2>/dev/null || true) + if [ "$vendor" != "0x1461" ]; then + continue + fi + if [ "$device" = "0x0054" ]; then + subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) + if [ "$subsystem" = "0x5700" ]; then + CARD_MODEL="gc570d" + else + CARD_MODEL="gc573" + fi + CARD_PCI=$(basename "$dev") + break + elif [ "$device" = "0x0055" ]; then + CARD_MODEL="gc573" + CARD_PCI=$(basename "$dev") + break + fi + done +} + +if [ -n "${CTL_BIN}" ] && [ -x "${CTL_BIN}" ]; then + detect_out="$("${CTL_BIN}" detect 2>/dev/null || true)" + CARD_MODEL="$(printf '%s\n' "${detect_out}" \ + | sed -n 's/.*Model Slug:[[:space:]]*//p' | head -n 1)" + CARD_PCI="$(printf '%s\n' "${detect_out}" \ + | sed -n 's/.*PCI BDF:[[:space:]]*//p' | head -n 1)" + case "${CARD_MODEL}" in + gc573|gc570d) ;; + *) CARD_MODEL="" ;; + esac + if printf '%s\n' "${detect_out}" | grep -q "Driver Bound: vfio-pci"; then + IS_BOUND=1 + fi +fi + +if [ -z "$CARD_MODEL" ]; then + detect_card_sysfs + if [ -n "$CARD_PCI" ]; then + driver_link="/sys/bus/pci/devices/${CARD_PCI}/driver" + if [ -L "$driver_link" ] && + [ "$(basename "$(readlink -f "$driver_link")")" = "vfio-pci" ]; then + IS_BOUND=1 + fi + fi +fi + +if [ -z "$CARD_MODEL" ]; then + echo "[!] everlive-service: No supported EverLive capture card detected." >&2 + exit 1 +fi + +echo "[*] everlive-service: Initializing ${CARD_MODEL} (${CARD_PCI:-unknown BDF})..." + +# 2. Select binaries and paths +if [ "$CARD_MODEL" = "gc570d" ]; then + if [ -x "${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" ]; then + ENGINE_BIN="${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" + elif [ -x "/usr/local/bin/opengc570d-userd" ]; then + ENGINE_BIN="/usr/local/bin/opengc570d-userd" + else + ENGINE_BIN="$(command -v opengc570d-userd 2>/dev/null || echo /usr/local/bin/opengc570d-userd)" + fi + BIND_SCRIPT="" + for candidate in \ + "${REPO_DIR}/tools/opengc570d-userd/gc570d-vfio-bind.sh" \ + "/usr/local/libexec/gc570d-vfio-bind.sh" \ + "/usr/local/bin/gc570d-vfio-bind.sh" \ + "/usr/libexec/gc570d-vfio-bind.sh" \ + "/usr/bin/gc570d-vfio-bind.sh"; do + [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break + done + ENGINE_ARGS=(--headless --loopback) +else + if [ -x "${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" ]; then + ENGINE_BIN="${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" + elif [ -x "/usr/local/bin/gc573_user_streamer" ]; then + ENGINE_BIN="/usr/local/bin/gc573_user_streamer" + else + ENGINE_BIN="$(command -v gc573_user_streamer 2>/dev/null || echo /usr/local/bin/gc573_user_streamer)" + fi + BIND_SCRIPT="" + for candidate in \ + "${REPO_DIR}/tools/opengc573-userd/vfio_pci_bind.sh" \ + "/usr/local/libexec/vfio_pci_bind.sh" \ + "/usr/local/bin/vfio_pci_bind.sh" \ + "/usr/libexec/vfio_pci_bind.sh" \ + "/usr/bin/vfio_pci_bind.sh"; do + [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break + done + ENGINE_ARGS=(--headless) +fi + +# 3. Ensure PCI device is bound to vfio-pci +if [ "$IS_BOUND" -eq 0 ] && [ -n "$CARD_PCI" ]; then + driver_link="/sys/bus/pci/devices/${CARD_PCI}/driver" + if [ ! -L "$driver_link" ] || [ "$(basename "$(readlink -f "$driver_link")")" != "vfio-pci" ]; then + echo "[*] everlive-service: Device not bound to vfio-pci. Executing bind script..." + if [ -x "${BIND_SCRIPT}" ]; then + if [ "$EUID" -ne 0 ]; then + if command -v pkexec >/dev/null 2>&1 && [ ! -t 0 ]; then + pkexec "${BIND_SCRIPT}" --bind || true + else + sudo "${BIND_SCRIPT}" --bind || true + fi + else + "${BIND_SCRIPT}" --bind || true + fi + fi + fi +fi + +# 4. Set memlock ulimit +ulimit -S -l unlimited 2>/dev/null || ulimit -S -l "$(ulimit -H -l)" 2>/dev/null || true + +# 5. Ensure cap_ipc_lock on engine binary +if [ -f "${ENGINE_BIN}" ] && command -v setcap >/dev/null 2>&1; then + if ! getcap "${ENGINE_BIN}" 2>/dev/null | grep -q "cap_ipc_lock"; then + echo "[*] everlive-service: Granting cap_ipc_lock=+ep to $(basename "${ENGINE_BIN}")..." + if [ "$EUID" -eq 0 ]; then + setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + elif command -v pkexec >/dev/null 2>&1 && [ ! -t 0 ]; then + pkexec setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + else + sudo setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + fi + fi +fi + +# 6. Execute engine daemon +echo "[+] everlive-service: Launching ${ENGINE_BIN} ${ENGINE_ARGS[*]}..." +exec "${ENGINE_BIN}" "${ENGINE_ARGS[@]}" "$@" diff --git a/tools/libeverlive-core/everlive.service b/tools/libeverlive-core/everlive.service new file mode 100644 index 0000000..2c5ac1d --- /dev/null +++ b/tools/libeverlive-core/everlive.service @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EverLive Universal Userspace Capture Service +# +# Powers up the capture hardware, holds the VFIO context open, enables monitor +# passthrough, and dispatches virtual cameras and PipeWire audio streams at login. +# +# Install with: +# systemctl --user daemon-reload +# systemctl --user enable --now everlive.service + +[Unit] +Description=EverLive Universal Userspace Capture Service +After=pipewire.service wireplumber.service +Wants=pipewire.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/everlive-service +Restart=on-failure +RestartSec=2 +LimitMEMLOCK=infinity + +[Install] +WantedBy=default.target diff --git a/tools/libeverlive-core/everlive_card.c b/tools/libeverlive-core/everlive_card.c new file mode 100644 index 0000000..0dd13fe --- /dev/null +++ b/tools/libeverlive-core/everlive_card.c @@ -0,0 +1,493 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_card.c — Core Hardware Registry and Autodiscovery for EverLive + */ + +#include "everlive_card.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_PCI_SYSFS_PATH "/sys/bus/pci/devices" + +/* ------------------------------------------------------------------------- + * Default Hardware Operations + * + * The core library owns discovery and the control-plane contract; a concrete + * backend (the GC573 streamer or the GC570D daemon) registers its vtable with + * everlive_register_card_ops(). Until then these defaults keep the interface + * well defined: probe() verifies the discovered identity, release() is a + * no-op because the core owns no hardware handles, and every operation that + * would touch real hardware reports -ENOTSUP instead of pretending success. + * ------------------------------------------------------------------------- */ +static int everlive_default_probe(struct everlive_device *dev) +{ + if (!dev || !dev->desc) + return -EINVAL; + if (!dev->pci_bdf[0]) + return -EINVAL; + return 0; +} + +static int everlive_default_unsupported(struct everlive_device *dev) +{ + (void)dev; + return -ENOTSUP; +} + +static int everlive_default_set_led(struct everlive_device *dev, + enum everlive_led_mode mode, + uint8_t r, uint8_t g, uint8_t b) +{ + (void)dev; + (void)mode; + (void)r; + (void)g; + (void)b; + return -ENOTSUP; +} + +static int everlive_default_set_input_port(struct everlive_device *dev, int port) +{ + (void)dev; + (void)port; + return -ENOTSUP; +} + +static int everlive_default_set_edid_profile(struct everlive_device *dev, + int port, + const char *profile_name, + const uint8_t *custom_bin, + size_t bin_len) +{ + (void)dev; + (void)port; + (void)profile_name; + (void)custom_bin; + (void)bin_len; + return -ENOTSUP; +} + +static int everlive_default_enable_passthrough(struct everlive_device *dev, + bool enable) +{ + (void)dev; + (void)enable; + return -ENOTSUP; +} + +static int everlive_default_get_telemetry(struct everlive_device *dev, + struct everlive_telemetry *out) +{ + (void)dev; + if (out) + memset(out, 0, sizeof(*out)); + return -ENOTSUP; +} + +static void everlive_default_release(struct everlive_device *dev) +{ + (void)dev; +} + +static const struct everlive_card_ops kEverliveDefaultOps = { + .probe = everlive_default_probe, + .init_hardware = everlive_default_unsupported, + .standby = everlive_default_unsupported, + .release = everlive_default_release, + .set_input_port = everlive_default_set_input_port, + .start_capture = everlive_default_unsupported, + .stop_capture = everlive_default_unsupported, + .set_edid_profile = everlive_default_set_edid_profile, + .enable_passthrough = everlive_default_enable_passthrough, + .set_led = everlive_default_set_led, + .get_telemetry = everlive_default_get_telemetry, +}; + +/* + * Backend registration table. Discovery descriptors are immutable, so a + * backend binds by model slug instead of mutating the static registry. + */ +#define EVERLIVE_MAX_OPS_BINDINGS 8 + +struct everlive_ops_binding { + const char *model_slug; + struct everlive_card_ops ops; /* copied, not borrowed */ +}; + +static struct everlive_ops_binding kEverliveOpsBindings[EVERLIVE_MAX_OPS_BINDINGS]; +static size_t kEverliveOpsBindingCount; + +/* + * Registration is a single-threaded startup step. The first lookup freezes + * the table so a backend cannot rebind while other threads are resolving + * operations. This is an enforcement guard, not a substitute for locking: + * callers must still register before starting any threads. + */ +static bool kEverliveOpsFrozen; + +/* ------------------------------------------------------------------------- + * Static Hardware Registry of Supported Capture Cards + * ------------------------------------------------------------------------- */ +static const struct everlive_device_desc kEverliveCardRegistry[] = { + { + .vendor_id = 0x1461, + .device_id = 0x0054, + .subsystem_id = 0x5700, + .model_slug = "gc570d", + .name = "AVerMedia Live Gamer DUO (GC570D)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | + EVERLIVE_CAP_DUAL_INPUT | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_HARDWARE_SPLITTER | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 2, + .ops = &kEverliveDefaultOps, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0054, + .subsystem_id = 0x5730, + .model_slug = "gc573", + .name = "AVerMedia Live Gamer 4K (GC573)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_1080P240 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_FRAMEGEN | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + /* A 0x0054 device with any other subsystem is a GC573. */ + .fallback_match = true, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0055, + .subsystem_id = 0, /* GC573 BOLT / Alternative PCIe ID */ + .model_slug = "gc573", + .name = "AVerMedia Live Gamer 4K / BOLT (GC573)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_1080P240 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_FRAMEGEN | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0057, + .subsystem_id = 0, /* Future expansion: Live Gamer 4K 2.1 */ + .model_slug = "gc575", + .name = "AVerMedia Live Gamer 4K 2.1 (GC575)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + }, +}; + +#define REGISTRY_COUNT (sizeof(kEverliveCardRegistry) / sizeof(kEverliveCardRegistry[0])) + +const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count) +{ + if (out_count) + *out_count = REGISTRY_COUNT; + return kEverliveCardRegistry; +} + +const struct everlive_device_desc * +everlive_find_card_by_slug(const char *model_slug) +{ + if (!model_slug || !*model_slug) + return NULL; + + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + if (strcmp(kEverliveCardRegistry[i].model_slug, model_slug) == 0) + return &kEverliveCardRegistry[i]; + } + + return NULL; +} + +int everlive_register_card_ops(const char *model_slug, + const struct everlive_card_ops *ops) +{ + const struct everlive_device_desc *canonical; + + if (!model_slug || !*model_slug || !ops) + return -EINVAL; + + canonical = everlive_find_card_by_slug(model_slug); + if (!canonical) + return -ENOENT; + + if (kEverliveOpsFrozen) + return -EBUSY; + + /* Replace an existing binding for the same model. */ + for (size_t i = 0; i < kEverliveOpsBindingCount; i++) { + if (kEverliveOpsBindings[i].model_slug == canonical->model_slug) { + kEverliveOpsBindings[i].ops = *ops; + return 0; + } + } + + if (kEverliveOpsBindingCount >= EVERLIVE_MAX_OPS_BINDINGS) + return -ENOSPC; + + kEverliveOpsBindings[kEverliveOpsBindingCount].model_slug = + canonical->model_slug; + kEverliveOpsBindings[kEverliveOpsBindingCount].ops = *ops; + kEverliveOpsBindingCount++; + return 0; +} + +const struct everlive_card_ops * +everlive_card_ops_for(const struct everlive_device_desc *desc) +{ + /* Any resolution closes the registry against further registration. */ + kEverliveOpsFrozen = true; + + if (!desc) + return &kEverliveDefaultOps; + + for (size_t i = 0; i < kEverliveOpsBindingCount; i++) { + if (kEverliveOpsBindings[i].model_slug == desc->model_slug || + strcmp(kEverliveOpsBindings[i].model_slug, + desc->model_slug) == 0) + return &kEverliveOpsBindings[i].ops; + } + + return desc->ops ? desc->ops : &kEverliveDefaultOps; +} + +/* + * @subsystem_known is false when sysfs could not be read. In that case an + * exact-subsystem entry can never match and a fallback entry is never used, + * so a missing ID file is not silently reported as a specific model. + */ +static const struct everlive_device_desc * +everlive_match_card_ex(uint16_t vendor, uint16_t device, + bool subsystem_known, uint16_t subsystem) +{ + /* Pass 1: exact match (vendor, device, subsystem) */ + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device && + d->subsystem_id != 0 && subsystem_known && + d->subsystem_id == subsystem) + return d; + } + + /* Pass 2: wildcard subsystem entries (match any/unknown subsystem) */ + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device && + d->subsystem_id == 0) + return d; + } + + /* Pass 3: per-device fallback, data-driven via fallback_match. This + * replaces the old hard-coded 0x0054 -> "gc573" special case. It only + * applies when the subsystem is known, so an unreadable sysfs file can + * never turn a GC570D into a GC573. + */ + if (subsystem_known) { + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = + &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device && + d->fallback_match) + return d; + } + } + + return NULL; +} + +const struct everlive_device_desc *everlive_match_card(uint16_t vendor, + uint16_t device, + uint16_t subsystem) +{ + return everlive_match_card_ex(vendor, device, true, subsystem); +} + +/* + * Read a 16-bit hex value from sysfs. Returns 0 on success and stores the + * value in @out, or a negative errno on failure. Callers must not confuse a + * read failure with the legitimate value 0 (which is a wildcard sentinel). + */ +static int read_sysfs_hex16(const char *path, uint16_t *out) +{ + FILE *f; + char buf[32]; + char *end = NULL; + unsigned long val; + + if (!path || !out) + return -EINVAL; + + f = fopen(path, "r"); + if (!f) + return -errno; + + if (!fgets(buf, sizeof(buf), f)) { + fclose(f); + return -EIO; + } + fclose(f); + + errno = 0; + val = strtoul(buf, &end, 16); + if (end == buf || errno == ERANGE || val > 0xffff) + return -EINVAL; + + while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') + end++; + if (*end != '\0') + return -EINVAL; + + *out = (uint16_t)val; + return 0; +} + +static int read_iommu_group(const char *dev_dir) +{ + char path[PATH_MAX]; + char target[PATH_MAX]; + + int n = snprintf(path, sizeof(path), "%s/iommu_group", dev_dir); + if (n < 0 || (size_t)n >= sizeof(path)) + return -1; + + ssize_t len = readlink(path, target, sizeof(target) - 1); + if (len <= 0) + return -1; + + target[len] = '\0'; + char *slash = strrchr(target, '/'); + if (!slash) + return -1; + + return (int)strtol(slash + 1, NULL, 10); +} + +bool everlive_is_pci_vfio_bound(const char *pci_bdf, const char *sysfs_pci_root) +{ + if (!pci_bdf || !*pci_bdf) + return false; + + const char *root = (sysfs_pci_root && *sysfs_pci_root) ? + sysfs_pci_root : DEFAULT_PCI_SYSFS_PATH; + + char driver_link[PATH_MAX]; + char target[PATH_MAX]; + + snprintf(driver_link, sizeof(driver_link), "%s/%s/driver", root, pci_bdf); + ssize_t len = readlink(driver_link, target, sizeof(target) - 1); + if (len <= 0) + return false; + + target[len] = '\0'; + char *slash = strrchr(target, '/'); + const char *driver_name = slash ? (slash + 1) : target; + + return (strcmp(driver_name, "vfio-pci") == 0); +} + +int everlive_detect_installed_cards(struct everlive_device *out_devices, + size_t max_devices, + const char *sysfs_pci_root) +{ + if (!out_devices || max_devices == 0) + return 0; + + const char *root = (sysfs_pci_root && *sysfs_pci_root) ? + sysfs_pci_root : DEFAULT_PCI_SYSFS_PATH; + + DIR *dir = opendir(root); + if (!dir) + return 0; + + size_t found_count = 0; + struct dirent *ent; + + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == '.') + continue; + + char dev_path[512]; + snprintf(dev_path, sizeof(dev_path), "%s/%s", root, ent->d_name); + + char vendor_path[PATH_MAX]; + char device_path[PATH_MAX]; + char subsystem_path[PATH_MAX]; + + snprintf(vendor_path, sizeof(vendor_path), "%s/vendor", dev_path); + snprintf(device_path, sizeof(device_path), "%s/device", dev_path); + snprintf(subsystem_path, sizeof(subsystem_path), "%s/subsystem_device", dev_path); + + uint16_t vendor = 0; + uint16_t device = 0; + uint16_t subsystem = 0; + bool subsystem_known; + + if (read_sysfs_hex16(vendor_path, &vendor) < 0 || + read_sysfs_hex16(device_path, &device) < 0) + continue; + + subsystem_known = + read_sysfs_hex16(subsystem_path, &subsystem) == 0; + + const struct everlive_device_desc *desc = + everlive_match_card_ex(vendor, device, + subsystem_known, subsystem); + + if (desc) { + struct everlive_device *dev = &out_devices[found_count]; + memset(dev, 0, sizeof(*dev)); + dev->desc = desc; + snprintf(dev->pci_bdf, sizeof(dev->pci_bdf), "%.31s", ent->d_name); + dev->iommu_group = read_iommu_group(dev_path); + dev->is_vfio_bound = everlive_is_pci_vfio_bound(ent->d_name, root); + + found_count++; + if (found_count >= max_devices) + break; + } + } + + closedir(dir); + return (int)found_count; +} diff --git a/tools/libeverlive-core/everlive_card.h b/tools/libeverlive-core/everlive_card.h new file mode 100644 index 0000000..e4bb95e --- /dev/null +++ b/tools/libeverlive-core/everlive_card.h @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_card.h — Core Hardware Abstraction Layer for EverLive Capture Cards + * + * Provides a polymorphic, modular interface for discovering, initializing, + * and managing PCIe and USB capture cards (GC573, GC570D, and future hardware). + */ + +#ifndef EVERLIVE_CARD_H +#define EVERLIVE_CARD_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- + * Hardware Capability Bitflags + * ------------------------------------------------------------------------- */ +#define EVERLIVE_CAP_VIDEO_CAPTURE (1U << 0) +#define EVERLIVE_CAP_AUDIO_CAPTURE (1U << 1) +#define EVERLIVE_CAP_PASSTHROUGH (1U << 2) +#define EVERLIVE_CAP_1080P60 (1U << 3) +#define EVERLIVE_CAP_4K60 (1U << 4) +#define EVERLIVE_CAP_1440P120 (1U << 5) +#define EVERLIVE_CAP_1080P240 (1U << 6) +#define EVERLIVE_CAP_HDR10 (1U << 7) +#define EVERLIVE_CAP_DUAL_INPUT (1U << 8) +#define EVERLIVE_CAP_RGB_LED (1U << 9) +#define EVERLIVE_CAP_BITSTREAM_AUDIO (1U << 10) +#define EVERLIVE_CAP_FRAMEGEN (1U << 11) +#define EVERLIVE_CAP_HARDWARE_SCALER (1U << 12) +#define EVERLIVE_CAP_HARDWARE_SPLITTER (1U << 13) +#define EVERLIVE_CAP_EDID_PASSTHROUGH (1U << 14) +#define EVERLIVE_CAP_4K120 (1U << 15) + +/* ------------------------------------------------------------------------- + * Lighting & Visual Feedback Modes + * ------------------------------------------------------------------------- */ +enum everlive_led_mode { + EVERLIVE_LED_MODE_OFF = 0, + EVERLIVE_LED_MODE_STATIC, + EVERLIVE_LED_MODE_BREATHING, + EVERLIVE_LED_MODE_RAINBOW, + EVERLIVE_LED_MODE_WAVE, +}; + +/* ------------------------------------------------------------------------- + * Pixel & Audio Formats + * ------------------------------------------------------------------------- */ +enum everlive_pixel_format { + EVERLIVE_PIX_FMT_UNKNOWN = 0, + EVERLIVE_PIX_FMT_YUYV, + EVERLIVE_PIX_FMT_NV12, + EVERLIVE_PIX_FMT_P010, + EVERLIVE_PIX_FMT_RGB24, + EVERLIVE_PIX_FMT_BGR24, + EVERLIVE_PIX_FMT_RGBA32, +}; + +struct everlive_video_timing { + uint32_t width; + uint32_t height; + double refresh_rate; + bool interlaced; + bool hdr10; + enum everlive_pixel_format format; +}; + +struct everlive_audio_timing { + uint32_t sample_rate; /* e.g. 48000 */ + uint8_t channels; /* e.g. 2, 6, 8 */ + uint8_t bit_depth; /* e.g. 16, 24 */ + bool is_compressed; /* true for AC-3 / DTS IEC 61937 */ + char codec_name[16]; /* e.g. "LPCM", "AC-3", "DTS" */ +}; + +/* ------------------------------------------------------------------------- + * Live Telemetry & Diagnostics + * ------------------------------------------------------------------------- */ +struct everlive_telemetry { + bool signal_locked; + int active_input_port; + struct everlive_video_timing video; + struct everlive_audio_timing audio; + double pci_bandwidth_mbps; + uint64_t frames_captured; + uint64_t frames_dropped; + float temperature_c; /* Negative value indicates unsupported */ +}; + +/* Forward declarations */ +struct everlive_device; +struct everlive_card_ops; + +/* ------------------------------------------------------------------------- + * Declarative Device Descriptor (Hardware Registry Entry) + * ------------------------------------------------------------------------- */ +struct everlive_device_desc { + uint16_t vendor_id; + uint16_t device_id; + uint16_t subsystem_id; /* 0 = wildcard match */ + const char *model_slug; /* Short identifier, e.g. "gc573", "gc570d" */ + const char *name; /* Friendly name, e.g. "AVerMedia Live Gamer 4K" */ + uint32_t capabilities; + int num_input_ports; + const struct everlive_card_ops *ops; + bool fallback_match; /* Last-resort match for this vendor/device */ +}; + +/* ------------------------------------------------------------------------- + * Polymorphic Hardware Operations Interface + * ------------------------------------------------------------------------- */ +struct everlive_card_ops { + /* Probe and identify the hardware at the given PCI/USB location */ + int (*probe)(struct everlive_device *dev); + + /* Initialize hardware registers, clock trees, and I2C buses */ + int (*init_hardware)(struct everlive_device *dev); + + /* Enter low-power standby or hand off to background daemon */ + int (*standby)(struct everlive_device *dev); + + /* Release hardware resources and close descriptors */ + void (*release)(struct everlive_device *dev); + + /* Select active HDMI input port (0-indexed) */ + int (*set_input_port)(struct everlive_device *dev, int port); + + /* Start DMA video and audio streaming pipelines */ + int (*start_capture)(struct everlive_device *dev); + + /* Stop streaming while keeping hardware awake */ + int (*stop_capture)(struct everlive_device *dev); + + /* Configure or bridge HDMI EDID */ + int (*set_edid_profile)(struct everlive_device *dev, int port, + const char *profile_name, + const uint8_t *custom_bin, size_t bin_len); + + /* Enable or disable zero-latency HDMI passthrough */ + int (*enable_passthrough)(struct everlive_device *dev, bool enable); + + /* Update RGB LED illumination */ + int (*set_led)(struct everlive_device *dev, enum everlive_led_mode mode, + uint8_t r, uint8_t g, uint8_t b); + + /* Retrieve real-time telemetry (signal lock, timings, stats) */ + int (*get_telemetry)(struct everlive_device *dev, + struct everlive_telemetry *out_telemetry); +}; + +/* ------------------------------------------------------------------------- + * Active Device Instance Handle + * ------------------------------------------------------------------------- */ +struct everlive_device { + const struct everlive_device_desc *desc; + char pci_bdf[32]; /* e.g. "0000:03:00.0" */ + int iommu_group; + bool is_vfio_bound; + void *priv; /* Backend-specific context */ +}; + +/* ------------------------------------------------------------------------- + * Registry & Discovery Functions + * ------------------------------------------------------------------------- */ + +/* Returns a pointer to the static registry table and sets out_count */ +const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count); + +/* Returns the registry entry for @model_slug, or NULL when unknown. */ +const struct everlive_device_desc * +everlive_find_card_by_slug(const char *model_slug); + +/* + * Bind a backend implementation to every registry entry with @model_slug. + * Backends own their hardware context; the core copies the vtable by value, + * so @ops only has to remain valid for the duration of this call. + * + * Registration is a startup operation: it must finish before the first + * everlive_card_ops_for() call, which is also the point where the library may + * start being used from multiple threads. No lock is taken, so callers must + * register before spawning threads. Once a lookup has happened the registry + * is frozen and later calls return -EBUSY. + * + * Returns 0, -EINVAL for bad arguments, -ENOENT for an unknown model, + * -EBUSY if the registry is already frozen, or -ENOSPC when the binding table + * is full. + */ +int everlive_register_card_ops(const char *model_slug, + const struct everlive_card_ops *ops); + +/* + * Resolve the active operations for a descriptor. This returns the most + * recently registered backend vtable, the descriptor's built-in vtable, or + * the core default (which reports -ENOTSUP for hardware actions) in that + * order, and never NULL. The first call freezes the registry; register all + * backends before calling this function. + */ +const struct everlive_card_ops * +everlive_card_ops_for(const struct everlive_device_desc *desc); + +/* Matches vendor, device, and subsystem IDs to a registered card descriptor */ +const struct everlive_device_desc *everlive_match_card(uint16_t vendor, + uint16_t device, + uint16_t subsystem); + +/* Scans sysfs (/sys/bus/pci/devices) and detects installed capture cards */ +int everlive_detect_installed_cards(struct everlive_device *out_devices, + size_t max_devices, + const char *sysfs_pci_root); + +/* Checks if a given PCI BDF is currently bound to vfio-pci */ +bool everlive_is_pci_vfio_bound(const char *pci_bdf, const char *sysfs_pci_root); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_CARD_H */ diff --git a/tools/libeverlive-core/everlive_ctl.c b/tools/libeverlive-core/everlive_ctl.c new file mode 100644 index 0000000..d8b2631 --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl.c @@ -0,0 +1,772 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl.c — Unified Control & Diagnostic CLI for EverLive Capture Cards + */ + +#include "everlive_card.h" +#include "everlive_ctl.h" +#include "everlive_ipc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int ctl_connect(char *path, size_t path_size); + +static void print_capabilities(uint32_t caps) +{ + printf(" Capabilities: "); + bool first = true; +#define CHECK_CAP(cap, name) \ + if (caps & (cap)) { \ + printf("%s%s", first ? "" : ", ", (name)); \ + first = false; \ + } + CHECK_CAP(EVERLIVE_CAP_VIDEO_CAPTURE, "Video-Capture"); + CHECK_CAP(EVERLIVE_CAP_AUDIO_CAPTURE, "Audio-Capture"); + CHECK_CAP(EVERLIVE_CAP_PASSTHROUGH, "Passthrough"); + CHECK_CAP(EVERLIVE_CAP_EDID_PASSTHROUGH, "EDID-Passthrough"); + CHECK_CAP(EVERLIVE_CAP_4K60, "4K60"); + CHECK_CAP(EVERLIVE_CAP_4K120, "4K120"); + CHECK_CAP(EVERLIVE_CAP_1440P120, "1440p120"); + CHECK_CAP(EVERLIVE_CAP_1080P240, "1080p240"); + CHECK_CAP(EVERLIVE_CAP_1080P60, "1080p60"); + CHECK_CAP(EVERLIVE_CAP_HDR10, "HDR10"); + CHECK_CAP(EVERLIVE_CAP_DUAL_INPUT, "Dual-Input"); + CHECK_CAP(EVERLIVE_CAP_RGB_LED, "RGB-LED"); + CHECK_CAP(EVERLIVE_CAP_BITSTREAM_AUDIO, "Bitstream-Audio (AC3/DTS)"); + CHECK_CAP(EVERLIVE_CAP_FRAMEGEN, "Lossless Scaling (lsfg-vk)"); + CHECK_CAP(EVERLIVE_CAP_HARDWARE_SPLITTER, "Hardware-Splitter"); +#undef CHECK_CAP + if (first) + printf("None"); + printf("\n"); +} + +/* + * Execute argv directly (no shell) and return its exit code, or -1 if it could + * not be spawned or reaped. When @quiet is true, stderr is discarded. + */ +static int run_command(const char *const argv[], bool quiet) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid < 0) + return -1; + + if (pid == 0) { + if (quiet) { + int devnull = open("/dev/null", O_WRONLY); + + if (devnull >= 0) { + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + } + execvp(argv[0], (char *const *)argv); + _exit(127); + } + + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -1; + } + + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +} + +static bool systemctl_unit_active(const char *unit) +{ + const char *const argv[] = { + "systemctl", "--user", "is-active", "--quiet", unit, NULL + }; + + return run_command(argv, true) == 0; +} + +static int cmd_detect(void) +{ + struct everlive_device cards[8]; + int count = everlive_detect_installed_cards(cards, 8, NULL); + + printf("=== EverLive Capture Card Discovery ===\n"); + if (count <= 0) { + printf("No supported EverLive capture cards detected on PCI bus.\n"); + return 1; + } + + printf("Found %d supported capture card%s:\n\n", count, count > 1 ? "s" : ""); + for (int i = 0; i < count; i++) { + const struct everlive_device *dev = &cards[i]; + printf("[%d] %s\n", i + 1, dev->desc->name); + printf(" Model Slug: %s\n", dev->desc->model_slug); + printf(" PCI BDF: %s\n", dev->pci_bdf); + printf(" IOMMU Group: %d\n", dev->iommu_group); + printf(" Driver Bound: %s\n", dev->is_vfio_bound ? "vfio-pci (Ready)" : "Kernel driver (Needs bind)"); + printf(" Input Ports: %d\n", dev->desc->num_input_ports); + print_capabilities(dev->desc->capabilities); + printf("\n"); + } + return 0; +} + +static void print_usage(const char *prog) +{ + printf("Usage: %s [arguments]\n\n" + "Hardware & Diagnostics:\n" + " detect List all installed capture cards and capabilities\n" + " list-models Display all supported card models in EverLive registry\n" + " status Display capture daemon status and active hardware\n\n" + "Runtime control (JSON-RPC to the running engine):\n" + " runtime status Engine status (signal, audio, V4L2, HDR...)\n" + " runtime led \n" + " runtime led-mode \n" + " runtime edid \n" + " runtime hdr \n" + " runtime hdr-ev <-2.0..2.0>\n" + " runtime hdr-passthrough <0|1>\n" + " runtime hotplug Pulse HDMI HPD and replay the EDID\n" + " runtime toast \n" + " runtime volume <0..150>\n" + " runtime mute <0|1>\n" + " runtime osd <0|1>\n\n" + "Service & Bringup:\n" + " init Initialize capture card in background (passthrough/audio)\n" + " service status Show systemd user service status\n" + " service start Start the background capture service\n" + " service stop Stop the background capture service\n" + " service enable Enable background capture service at user login\n" + " service disable Disable background capture autostart\n" + " help Show this help message\n\n", prog); +} + +static int cmd_status(void) +{ + char path[108]; + int fd; + + printf("=== EverLive Background Engine Status ===\n"); + + /* The engine is running when its JSON-RPC control socket answers. */ + fd = ctl_connect(path, sizeof(path)); + if (fd >= 0) { + close(fd); + printf("Active Process: EverLive engine (running at %s)\n", + path); + } else { + printf("Active Process: None (Engine is idle/standby)\n"); + } + + /* Check systemd user service */ + printf("Service State: "); + if (systemctl_unit_active("everlive.service") || + systemctl_unit_active("opengc570d-userd.service")) + printf("active\n"); + else + printf("inactive\n"); + + /* Also report detected card */ + printf("\n"); + cmd_detect(); + return 0; +} + +/* + * Locate the background service script. Prefer one sitting next to the + * running everlive-ctl binary (true for both an installed tree and a source + * build), then the installed locations, then the legacy CWD-relative path. + */ +static bool resolve_service_script(char *out, size_t out_size) +{ + static const char *const candidates[] = { + "/usr/local/bin/everlive-service", + "/usr/local/bin/everlive-service.sh", + "./tools/libeverlive-core/everlive-service.sh", + }; + char exe_path[PATH_MAX]; + ssize_t len; + + len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len > 0) { + char *slash; + + exe_path[len] = '\0'; + slash = strrchr(exe_path, '/'); + if (slash) { + int n; + + *slash = '\0'; + n = snprintf(out, out_size, "%s/everlive-service", + exe_path); + if (n > 0 && (size_t)n < out_size && + access(out, X_OK) == 0) + return true; + n = snprintf(out, out_size, "%s/everlive-service.sh", + exe_path); + if (n > 0 && (size_t)n < out_size && + access(out, X_OK) == 0) + return true; + } + } + + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + int n = snprintf(out, out_size, "%s", candidates[i]); + + if (n > 0 && (size_t)n < out_size && access(out, X_OK) == 0) + return true; + } + + return false; +} + +/* + * Launch @path detached from the terminal. A pipe with O_CLOEXEC tells the + * parent whether exec() actually succeeded: EOF means success, four bytes + * mean the child failed with that errno. The parent does not wait, so the + * service is not tied to the lifetime of this CLI. + */ +static int spawn_detached(const char *path) +{ + int pipefd[2]; + pid_t pid; + int err = 0; + ssize_t n; + + if (pipe2(pipefd, O_CLOEXEC) < 0) + return -1; + + pid = fork(); + if (pid < 0) { + int saved = errno; + + close(pipefd[0]); + close(pipefd[1]); + errno = saved; + return -1; + } + + if (pid == 0) { + int devnull; + + close(pipefd[0]); + if (setsid() < 0) + goto fail; + + devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + + execl(path, path, (char *)NULL); +fail: + err = errno; + (void)!write(pipefd[1], &err, sizeof(err)); + _exit(127); + } + + close(pipefd[1]); + do { + n = read(pipefd[0], &err, sizeof(err)); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + int saved = errno; + + close(pipefd[0]); + errno = saved; + return -1; + } + close(pipefd[0]); + + /* Any data means the child failed before exec(); EOF means success. */ + if (n > 0) { + errno = err; + return -1; + } + + return 0; +} + +static int cmd_init(void) +{ + const char *const everlive_enabled[] = { + "systemctl", "--user", "is-enabled", "--quiet", + "everlive.service", NULL + }; + const char *const gc570d_enabled[] = { + "systemctl", "--user", "is-enabled", "--quiet", + "opengc570d-userd.service", NULL + }; + const char *const everlive_start[] = { + "systemctl", "--user", "start", "everlive.service", NULL + }; + const char *const gc570d_start[] = { + "systemctl", "--user", "start", + "opengc570d-userd.service", NULL + }; + char script_path[PATH_MAX]; + char path[108]; + int fd; + + printf("[*] Initializing EverLive capture hardware in background...\n"); + + /* The engine is already up if its control socket answers. */ + fd = ctl_connect(path, sizeof(path)); + if (fd >= 0) { + close(fd); + printf("[+] Capture engine is already running and hardware is initialized.\n"); + return 0; + } + + /* Try starting via systemd if a user unit is installed. */ + if (run_command(everlive_enabled, true) == 0 || + run_command(gc570d_enabled, true) == 0) { + printf("[*] Starting via systemd user service...\n"); + if (run_command(everlive_start, true) == 0 || + run_command(gc570d_start, false) == 0) { + printf("[+] EverLive background service started.\n"); + return 0; + } + fprintf(stderr, + "Failed to start the EverLive systemd user service.\n"); + return 1; + } + + /* Direct script invocation */ + + if (!resolve_service_script(script_path, sizeof(script_path))) { + fprintf(stderr, + "Could not locate everlive-service; install it or run from the source tree.\n"); + return 1; + } + + printf("[*] Spawning %s...\n", script_path); + if (spawn_detached(script_path) < 0) { + fprintf(stderr, "Failed to launch %s: %s\n", + script_path, strerror(errno)); + return 1; + } + + printf("[+] EverLive background service launched successfully.\n"); + return 0; +} + +static int cmd_service(int argc, char **argv) +{ + const char *action; + + if (argc < 3) + action = "status"; + else + action = argv[2]; + + if (strcmp(action, "status") == 0) { + const char *const status_argv[] = { + "systemctl", "--user", "status", "everlive.service", + "opengc570d-userd.service", NULL + }; + + return run_command(status_argv, true); + } + + if (strcmp(action, "start") == 0) { + const char *const everlive[] = { + "systemctl", "--user", "start", "everlive.service", NULL + }; + const char *const gc570d[] = { + "systemctl", "--user", "start", + "opengc570d-userd.service", NULL + }; + + if (run_command(everlive, true) == 0) + return 0; + return run_command(gc570d, false); + } + + if (strcmp(action, "stop") == 0) { + const char *const stop_argv[] = { + "systemctl", "--user", "stop", "everlive.service", + "opengc570d-userd.service", NULL + }; + + return run_command(stop_argv, true); + } + + if (strcmp(action, "enable") == 0) { + const char *const everlive[] = { + "systemctl", "--user", "enable", "--now", + "everlive.service", NULL + }; + const char *const gc570d[] = { + "systemctl", "--user", "enable", "--now", + "opengc570d-userd.service", NULL + }; + + if (run_command(everlive, true) == 0) + return 0; + return run_command(gc570d, false); + } + + if (strcmp(action, "disable") == 0) { + const char *const disable_argv[] = { + "systemctl", "--user", "disable", "--now", + "everlive.service", "opengc570d-userd.service", NULL + }; + + return run_command(disable_argv, true); + } + + fprintf(stderr, "Unknown service action: %s\n", action); + return 1; +} + +static int cmd_list_models(void) +{ + size_t count = 0; + const struct everlive_device_desc *reg = everlive_get_card_registry(&count); + + printf("=== EverLive Supported Card Registry ===\n"); + for (size_t i = 0; i < count; i++) { + printf("- %s (%s)\n", reg[i].name, reg[i].model_slug); + printf(" PCI ID: 0x%04x:0x%04x (Subsystem: 0x%04x)\n", + reg[i].vendor_id, reg[i].device_id, reg[i].subsystem_id); + print_capabilities(reg[i].capabilities); + } + return 0; +} + +static int ctl_resolve_socket(char *buf, size_t size) +{ + return everlive_ipc_resolve_socket_path(buf, size, NULL) == 0 ? 0 : -1; +} + +/* + * Resolve and connect to the engine control socket. Returns the fd on + * success, or -1 with errno set. + */ +static int ctl_connect(char *path, size_t path_size) +{ + struct sockaddr_un addr; + int fd; + int saved; + + if (ctl_resolve_socket(path, path_size) < 0) { + errno = ENAMETOOLONG; + return -1; + } + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + saved = errno; + close(fd); + errno = saved; + return -1; + } + + return fd; +} + +static int ctl_call(const char *method, const char *params) +{ + char path[108] = ""; + char request[2048]; + char response[8192]; + size_t off = 0; + size_t request_len; + int fd; + + fd = ctl_connect(path, sizeof(path)); + if (fd < 0) { + if (errno == ENAMETOOLONG) { + fprintf(stderr, + "EverLive control socket path is too long\n"); + } else { + fprintf(stderr, + "Cannot connect to the EverLive engine at %s: %s\n" + "Is the capture engine running?\n", + path, strerror(errno)); + } + return 1; + } + + if (everlive_ctl_build_request(method, params, request, + sizeof(request)) < 0) { + fprintf(stderr, "Request too long\n"); + close(fd); + return 1; + } + + request_len = strlen(request); + + while (off < request_len) { + ssize_t n = write(fd, request + off, request_len - off); + + if (n < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "write: %s\n", strerror(errno)); + close(fd); + return 1; + } + if (n == 0) { + fprintf(stderr, "write: no progress\n"); + close(fd); + return 1; + } + off += (size_t)n; + } + + off = 0; + for (;;) { + ssize_t n; + + if (off + 1 >= sizeof(response)) { + fprintf(stderr, "Response too large\n"); + close(fd); + return 1; + } + n = read(fd, response + off, sizeof(response) - off - 1); + if (n < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "read: %s\n", strerror(errno)); + close(fd); + return 1; + } + if (n == 0) + break; + off += (size_t)n; + if (memchr(response, '\n', off)) + break; + } + close(fd); + + response[off] = '\0'; + while (off > 0 && (response[off - 1] == '\n' || response[off - 1] == '\r')) + response[--off] = '\0'; + + printf("%s\n", response); + return everlive_ctl_response_has_error(response) ? 1 : 0; +} + +static int parse_switch(const char *value) +{ + if (!value) + return -1; + if (!strcasecmp(value, "on") || !strcasecmp(value, "true") || + !strcasecmp(value, "1") || !strcasecmp(value, "yes") || + !strcasecmp(value, "enable") || !strcasecmp(value, "enabled")) + return 1; + if (!strcasecmp(value, "off") || !strcasecmp(value, "false") || + !strcasecmp(value, "0") || !strcasecmp(value, "no") || + !strcasecmp(value, "disable") || !strcasecmp(value, "disabled")) + return 0; + return -1; +} + +static int cmd_runtime(int argc, char **argv) +{ + char params[1536]; + char escaped[512]; + + if (argc < 3) { + fprintf(stderr, "Usage: everlive-ctl runtime [value]\n" + " status Show the engine JSON status\n" + " led Set the static LED colour\n" + " led-mode \n" + " edid \n" + " hdr Toggle HDR processing\n" + " hdr-ev <-2.0..2.0> Set tonemap exposure in EV\n" + " hdr-passthrough <0|1> Toggle BT.2020 HDR passthrough\n" + " hotplug Pulse HDMI HPD / replay EDID\n" + " toast Show an on-screen notification\n" + " volume <0..150> Set desktop audio volume %%\n" + " mute <0|1> Mute/unmute desktop audio\n" + " osd <0|1> Toggle the on-screen overlay\n"); + return 1; + } + + const char *topic = argv[2]; + + if (strcmp(topic, "status") == 0) + return ctl_call("status.get", "{}"); + + if (strcmp(topic, "led") == 0) { + unsigned long rgb; + + if (argc < 4) { + fprintf(stderr, "runtime led needs RRGGBB or off\n"); + return 1; + } + if (strcasecmp(argv[3], "off") == 0) + return ctl_call("led.mode.set", "{\"mode\":\"off\"}"); + if (everlive_ctl_parse_hex_rgb(argv[3], &rgb) < 0 || + everlive_ctl_format_led(rgb, params, sizeof(params)) < 0) { + fprintf(stderr, "runtime led needs RRGGBB or off\n"); + return 1; + } + return ctl_call("led.set", params); + } + + if (strcmp(topic, "led-mode") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime led-mode needs static|wave|rainbow|off\n"); + return 1; + } + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); + return ctl_call("led.mode.set", params); + } + + if (strcmp(topic, "edid") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime edid needs a profile name or path\n"); + return 1; + } + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"profile\":\"%s\"}", escaped); + return ctl_call("edid.set", params); + } + + if (strcmp(topic, "hdr") == 0) { + const char *mode = argc >= 4 ? argv[3] : "auto"; + + everlive_ctl_json_escape(mode, escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); + return ctl_call("hdr.set", params); + } + + if (strcmp(topic, "hdr-ev") == 0) { + double ev; + + if (argc < 4) { + fprintf(stderr, "runtime hdr-ev needs an EV value\n"); + return 1; + } + if (everlive_ctl_parse_double(argv[3], &ev) < 0 || + everlive_ctl_format_exposure(ev, params, sizeof(params)) < 0) { + fprintf(stderr, "runtime hdr-ev needs a numeric EV value\n"); + return 1; + } + return ctl_call("hdr.exposure", params); + } + + if (strcmp(topic, "hdr-passthrough") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime hdr-passthrough needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"enabled\":%s}", on ? "true" : "false"); + return ctl_call("hdr.passthrough", params); + } + + if (strcmp(topic, "hotplug") == 0) + return ctl_call("hotplug", "{\"pulse\":true}"); + + if (strcmp(topic, "toast") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime toast needs text\n"); + return 1; + } + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"message\":\"%s\"}", escaped); + return ctl_call("toast.show", params); + } + + if (strcmp(topic, "volume") == 0) { + double percent; + + if (argc < 4) { + fprintf(stderr, "runtime volume needs 0..150\n"); + return 1; + } + if (everlive_ctl_parse_double(argv[3], &percent) < 0 || + everlive_ctl_format_volume(percent, params, + sizeof(params)) < 0) { + fprintf(stderr, + "runtime volume needs a number in 0..150\n"); + return 1; + } + return ctl_call("audio.volume.set", params); + } + + if (strcmp(topic, "mute") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime mute needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"muted\":%s}", on ? "true" : "false"); + return ctl_call("audio.mute.set", params); + } + + if (strcmp(topic, "osd") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime osd needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"enabled\":%s}", on ? "true" : "false"); + return ctl_call("osd.set", params); + } + + fprintf(stderr, "Unknown runtime topic: %s\n", topic); + return 1; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + return cmd_detect(); + } + + const char *cmd = argv[1]; + if (strcmp(cmd, "detect") == 0 || strcmp(cmd, "-d") == 0) { + return cmd_detect(); + } else if (strcmp(cmd, "status") == 0 || strcmp(cmd, "-s") == 0) { + return cmd_status(); + } else if (strcmp(cmd, "init") == 0 || strcmp(cmd, "-i") == 0) { + return cmd_init(); + } else if (strcmp(cmd, "service") == 0) { + return cmd_service(argc, argv); + } else if (strcmp(cmd, "list-models") == 0) { + return cmd_list_models(); + } else if (strcmp(cmd, "runtime") == 0 || strcmp(cmd, "ctl") == 0) { + return cmd_runtime(argc, argv); + } else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) { + print_usage(argv[0]); + return 0; + } else { + fprintf(stderr, "Unknown command: %s\n\n", cmd); + print_usage(argv[0]); + return 1; + } +} diff --git a/tools/libeverlive-core/everlive_ctl.h b/tools/libeverlive-core/everlive_ctl.h new file mode 100644 index 0000000..1ee0b47 --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl.h — JSON-RPC request helpers for everlive-ctl + * + * These helpers are stateless and socket-free so they can be unit tested + * without a running capture engine. + */ + +#ifndef EVERLIVE_CTL_H +#define EVERLIVE_CTL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Serialize a JSON-RPC 2.0 request. Returns 0 on success, -1 if the result + * does not fit in @out. */ +int everlive_ctl_build_request(const char *method, const char *params, + char *out, size_t out_size); + +/* Escape @in for embedding inside a JSON string literal. */ +void everlive_ctl_json_escape(const char *in, char *out, size_t out_size); + +/* True when the response has a top-level-style "error" key (quoted key + * followed by a colon). Ignores the word "error" appearing in values. */ +bool everlive_ctl_response_has_error(const char *json); + +/* Parse a finite base-10 double, rejecting trailing garbage and NaN/Inf. + * Returns 0 on success, -1 on invalid input. */ +int everlive_ctl_parse_double(const char *value, double *out); + +/* Parse a 24-bit RRGGBB colour given in hex. Returns 0 on success. */ +int everlive_ctl_parse_hex_rgb(const char *value, unsigned long *out); + +/* Build the "params" object for the numeric runtime commands. Each returns 0 + * on success or -1 on invalid input / insufficient buffer space. */ +int everlive_ctl_format_exposure(double ev, char *out, size_t out_size); +int everlive_ctl_format_volume(double percent, char *out, size_t out_size); +int everlive_ctl_format_led(unsigned long rgb, char *out, size_t out_size); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_CTL_H */ diff --git a/tools/libeverlive-core/everlive_ctl_proto.c b/tools/libeverlive-core/everlive_ctl_proto.c new file mode 100644 index 0000000..eb6e2bc --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl_proto.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl_proto.c — Stateless JSON-RPC request helpers for everlive-ctl + * + * Kept separate from everlive_ctl.c so the serialization and validation logic + * can be exercised by the unit tests without a control socket. + */ + +#include "everlive_ctl.h" +#include "everlive_json.h" + +#include +#include +#include +#include +#include + +int everlive_ctl_build_request(const char *method, const char *params, + char *out, size_t out_size) +{ + int n; + + if (!method || !out || out_size == 0) + return -1; + + n = snprintf(out, out_size, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + method, params && *params ? params : "{}"); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +void everlive_ctl_json_escape(const char *in, char *out, size_t out_size) +{ + everlive_json_escape_into(out, out_size, in); +} + +bool everlive_ctl_response_has_error(const char *json) +{ + return everlive_json_has_top_level_key(json, "error"); +} + +int everlive_ctl_parse_double(const char *value, double *out) +{ + char *end = NULL; + double v; + + if (!value || !*value || !out) + return -1; + + errno = 0; + v = strtod(value, &end); + if (end == value || *end != '\0' || errno == ERANGE) + return -1; + + /* NaN and +/-Inf are not valid JSON numbers. */ + if (!(v >= -DBL_MAX && v <= DBL_MAX)) + return -1; + + *out = v; + return 0; +} + +int everlive_ctl_parse_hex_rgb(const char *value, unsigned long *out) +{ + char *end = NULL; + unsigned long v; + + if (!value || !*value || !out) + return -1; + + errno = 0; + v = strtoul(value, &end, 16); + if (end == value || *end != '\0' || errno == ERANGE || v > 0xffffffUL) + return -1; + + *out = v; + return 0; +} + +int everlive_ctl_format_exposure(double ev, char *out, size_t out_size) +{ + int n = snprintf(out, out_size, "{\"exposure\":%g}", ev); + + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +int everlive_ctl_format_volume(double percent, char *out, size_t out_size) +{ + int n; + + if (!(percent >= 0.0 && percent <= 150.0)) + return -1; + + n = snprintf(out, out_size, "{\"gain\":%g}", percent / 100.0); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +int everlive_ctl_format_led(unsigned long rgb, char *out, size_t out_size) +{ + int n; + + if (rgb > 0xffffffUL) + return -1; + + n = snprintf(out, out_size, "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", + (rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} diff --git a/tools/libeverlive-core/everlive_frame.c b/tools/libeverlive-core/everlive_frame.c new file mode 100644 index 0000000..4bdb53e --- /dev/null +++ b/tools/libeverlive-core/everlive_frame.c @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "everlive_frame.h" + +static inline uint8_t clamp_u8(int val) +{ + if (val < 0) + return 0; + if (val > 255) + return 255; + return (uint8_t)val; +} + +/* Arithmetic right shift by 8 without relying on implementation-defined + * behaviour for negative operands. */ +static inline int shift8(int val) +{ + return val >= 0 ? (val >> 8) : -(((-val) + 255) >> 8); +} + +void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int height) +{ + if (!src || !dst || width <= 0 || height <= 0) + return; + + /* YUY2 packs two pixels into each 4-byte macro-pixel, so an odd width + * cannot be represented; reject it rather than desynchronising rows. */ + if ((width & 1) != 0) + return; + + size_t total_pairs = ((size_t)width * (size_t)height) / 2; + const uint8_t *s = src; + uint8_t *d = dst; + + for (size_t i = 0; i < total_pairs; i++) { + int y0 = s[0]; + int u = s[1] - 128; + int y1 = s[2]; + int v = s[3] - 128; + s += 4; + + int r_off = shift8(359 * v); + int g_off = shift8(88 * u + 183 * v); + int b_off = shift8(454 * u); + + /* Pixel 0 */ + d[0] = clamp_u8(y0 + r_off); + d[1] = clamp_u8(y0 - g_off); + d[2] = clamp_u8(y0 + b_off); + + /* Pixel 1 */ + d[3] = clamp_u8(y1 + r_off); + d[4] = clamp_u8(y1 - g_off); + d[5] = clamp_u8(y1 + b_off); + d += 6; + } +} + +void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, + int src_w, int src_h, + int dst_w, int dst_h) +{ + if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0 || (src_w & 1)) + return; + + /* Fixed-point step in 16.16 format */ + uint32_t step_x = ((uint32_t)src_w << 16) / (uint32_t)dst_w; + uint32_t step_y = ((uint32_t)src_h << 16) / (uint32_t)dst_h; + + uint32_t src_y_fp = 0; + for (int dy = 0; dy < dst_h; dy++) { + int sy = (int)(src_y_fp >> 16); + if (sy >= src_h) + sy = src_h - 1; + src_y_fp += step_y; + + const uint8_t *row = src + (size_t)sy * src_w * 2; + uint8_t *d = dst + (size_t)dy * dst_w * 3; + uint32_t src_x_fp = 0; + + for (int dx = 0; dx < dst_w; dx++) { + int sx = (int)(src_x_fp >> 16); + if (sx >= src_w) + sx = src_w - 1; + src_x_fp += step_x; + + /* In YUY2, pair index is (sx & ~1) * 2 */ + int pair_base = (sx & ~1) * 2; + int y = (sx & 1) ? row[pair_base + 2] : row[pair_base + 0]; + int u = row[pair_base + 1] - 128; + int v = row[pair_base + 3] - 128; + + int r_off = shift8(359 * v); + int g_off = shift8(88 * u + 183 * v); + int b_off = shift8(454 * u); + + d[0] = clamp_u8(y + r_off); + d[1] = clamp_u8(y - g_off); + d[2] = clamp_u8(y + b_off); + d += 3; + } + } +} diff --git a/tools/libeverlive-core/everlive_frame.h b/tools/libeverlive-core/everlive_frame.h new file mode 100644 index 0000000..f41293c --- /dev/null +++ b/tools/libeverlive-core/everlive_frame.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef EVERLIVE_FRAME_H +#define EVERLIVE_FRAME_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Convert packed YUY2 (YUYV) 4:2:2 buffer to packed 24-bit RGB (RGB888). + * + * Fast fixed-point conversion using the BT.601 color matrix. + * + * @param src Pointer to source YUY2 buffer (width * height * 2 bytes). + * @param dst Pointer to destination RGB888 buffer (width * height * 3 bytes). + * @param width Frame width in pixels (must be even). + * @param height Frame height in pixels. + */ +void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int height); + +/** + * Convert and downscale packed YUY2 (YUYV) buffer to packed 24-bit RGB (RGB888). + * + * Uses nearest-neighbour point sampling for low-overhead preview scaling. + * + * @param src Pointer to source YUY2 buffer. + * @param dst Pointer to destination RGB888 buffer. + * @param src_w Source width in pixels. + * @param src_h Source height in pixels. + * @param dst_w Destination width in pixels. + * @param dst_h Destination height in pixels. + */ +void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, + int src_w, int src_h, + int dst_w, int dst_h); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_FRAME_H */ diff --git a/tools/libeverlive-core/everlive_ipc.c b/tools/libeverlive-core/everlive_ipc.c new file mode 100644 index 0000000..96537a3 --- /dev/null +++ b/tools/libeverlive-core/everlive_ipc.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ipc.c — Shared control-socket path resolution for EverLive + */ + +#include "everlive_ipc.h" + +#include +#include +#include +#include +#include +#include + +static int ipc_copy(char *buf, size_t size, const char *value) +{ + int n; + + if (!buf || size == 0) + return -EINVAL; + + n = snprintf(buf, size, "%s", value); + if (n < 0 || (size_t)n >= size || (size_t)n >= EVERLIVE_IPC_PATH_MAX) + return -ENAMETOOLONG; + + return 0; +} + +int everlive_ipc_runtime_dir(char *buf, size_t size) +{ + const char *xdg; + char candidate[EVERLIVE_IPC_PATH_MAX]; + int n; + + if (!buf || size == 0) + return -EINVAL; + + xdg = getenv("XDG_RUNTIME_DIR"); + if (xdg && *xdg && access(xdg, W_OK) == 0) + return ipc_copy(buf, size, xdg); + + n = snprintf(candidate, sizeof(candidate), "/run/user/%u", + (unsigned int)getuid()); + if (n > 0 && (size_t)n < sizeof(candidate) && + access(candidate, W_OK) == 0) + return ipc_copy(buf, size, candidate); + + n = snprintf(candidate, sizeof(candidate), "/tmp/everlive-%u", + (unsigned int)getuid()); + if (n < 0 || (size_t)n >= sizeof(candidate)) + return -ENAMETOOLONG; + + return ipc_copy(buf, size, candidate); +} + +int everlive_ipc_resolve_socket_path(char *buf, size_t size, + const char *override_path) +{ + const char *env; + char dir[EVERLIVE_IPC_PATH_MAX]; + int n; + int ret; + + if (!buf || size == 0) + return -EINVAL; + + if (override_path && *override_path) + return ipc_copy(buf, size, override_path); + + env = getenv("EVERLIVE_SOCKET"); + if (env && *env) + return ipc_copy(buf, size, env); + + env = getenv("EVERLIVE_CONTROL_SOCKET"); + if (env && *env) + return ipc_copy(buf, size, env); + + ret = everlive_ipc_runtime_dir(dir, sizeof(dir)); + if (ret < 0) + return ret; + + n = snprintf(buf, size, "%s/%s", dir, EVERLIVE_IPC_SOCKET_NAME); + if (n < 0 || (size_t)n >= size || (size_t)n >= EVERLIVE_IPC_PATH_MAX) + return -ENAMETOOLONG; + + return 0; +} + +int everlive_ipc_prepare_socket_path(char *buf, size_t size, + const char *override_path) +{ + char dir[EVERLIVE_IPC_PATH_MAX]; + char *slash; + int ret; + + ret = everlive_ipc_resolve_socket_path(buf, size, override_path); + if (ret < 0) + return ret; + + if (snprintf(dir, sizeof(dir), "%s", buf) < 0) + return -EINVAL; + + slash = strrchr(dir, '/'); + if (!slash || slash == dir) + return 0; + + *slash = '\0'; + if (mkdir(dir, 0700) < 0 && errno != EEXIST) + return -errno; + + return 0; +} diff --git a/tools/libeverlive-core/everlive_ipc.h b/tools/libeverlive-core/everlive_ipc.h new file mode 100644 index 0000000..65576bf --- /dev/null +++ b/tools/libeverlive-core/everlive_ipc.h @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ipc.h — Shared control-socket path resolution for EverLive + * + * Both engines bind and every client (everlive-ctl, the Python hub, shell + * helpers) connects through the same deterministic path. The resolver never + * probes the socket itself: a path must be valid before the socket exists. + */ + +#ifndef EVERLIVE_IPC_H +#define EVERLIVE_IPC_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define EVERLIVE_IPC_SOCKET_NAME "everlive.sock" +#define EVERLIVE_IPC_PATH_MAX 108 /* sizeof sun_path */ + +/* + * Runtime directory, without touching the socket: + * $XDG_RUNTIME_DIR (if set and writable) + * /run/user/ (if writable) + * /tmp/everlive- + * + * Returns 0, -EINVAL for bad arguments, or -ENAMETOOLONG when it does not + * fit in @buf. + */ +int everlive_ipc_runtime_dir(char *buf, size_t size); + +/* + * Pure path resolution: + * 1. @override_path + * 2. $EVERLIVE_SOCKET (full path; used by everlive_client.py) + * 3. $EVERLIVE_CONTROL_SOCKET (legacy full path) + * 4. /everlive.sock + * + * Returns 0, -EINVAL on bad arguments, or -ENAMETOOLONG when it does not fit. + */ +int everlive_ipc_resolve_socket_path(char *buf, size_t size, + const char *override_path); + +/* + * Same as resolve, then ensures the parent directory exists (mkdir 0700, + * EEXIST is success). Intended for the server side before bind(); clients + * must use the pure resolver so they never create directories. + */ +int everlive_ipc_prepare_socket_path(char *buf, size_t size, + const char *override_path); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_IPC_H */ diff --git a/tools/libeverlive-core/everlive_json.c b/tools/libeverlive-core/everlive_json.c new file mode 100644 index 0000000..ec35f40 --- /dev/null +++ b/tools/libeverlive-core/everlive_json.c @@ -0,0 +1,443 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_json.c — jsmn-backed JSON-RPC 2.0 scanners and formatters + * + * The wire protocol is newline-delimited JSON-RPC 2.0 over AF_UNIX. Requests + * are flat objects, so a token scan is enough; a full parser would add a + * dependency for no benefit. Unknown fields are ignored and the raw "id" + * token is echoed back verbatim. + */ + +#include "everlive_json.h" + +#include +#include +#include +#include +#include + +/* + * The jsmntok_t layout depends on JSMN_PARENT_LINKS. It MUST match + * third_party/jsmn.o, which defines the implementation; this file only needs + * the declarations plus the same struct layout. + */ +#define JSMN_PARENT_LINKS +#define JSMN_HEADER +#include "third_party/jsmn.h" + +typedef enum { + JSON_GET_STRING, + JSON_GET_LONG, + JSON_GET_DOUBLE, + JSON_GET_BOOL, + JSON_GET_RAW +} json_get_type; + +/* + * Find a key in the JSON token tree. Direct children of the root object are + * matched first so a value string that happens to equal the key name (for + * example {"params":{"text":"method"},"method":"led.set"}) can never shadow + * the real top-level key. + * + * With JSMN_PARENT_LINKS a key token's parent is the containing object, while + * a value token's parent is the preceding key token. Matching on + * parent == 0 therefore selects root-level keys only. + */ +static int json_token_find_key(const jsmntok_t *tokens, int tok_count, + const char *key, const char *json) +{ + const size_t klen = strlen(key); + + /* Pass 1: keys of the root object (token 0). */ + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; + } + } + + /* Pass 2: keys of any nested object (e.g. inside "params"). */ + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING) + continue; + if (tokens[i].parent <= 0 || + tokens[tokens[i].parent].type != JSMN_OBJECT) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; + } + } + + return -1; +} + +static bool json_token_to_string(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) +{ + int len; + + if (!tok || tok->type != JSMN_STRING) + return false; + len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) + return false; + memcpy(out, json + tok->start, (size_t)len); + out[len] = '\0'; + return true; +} + +static bool json_token_to_long(const jsmntok_t *tok, const char *json, + long *out) +{ + char buf[64]; + char *end; + long val; + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, (size_t)len); + buf[len] = '\0'; + errno = 0; + val = strtol(buf, &end, 10); + if (errno || end == buf || *end != '\0') + return false; + *out = val; + return true; +} + +static bool json_token_to_double(const jsmntok_t *tok, const char *json, + double *out) +{ + char buf[64]; + char *end; + double val; + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, (size_t)len); + buf[len] = '\0'; + errno = 0; + val = strtod(buf, &end); + if (errno || end == buf || *end != '\0' || !isfinite(val)) + return false; + *out = val; + return true; +} + +static bool json_token_to_bool(const jsmntok_t *tok, const char *json, + bool *out) +{ + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len == 4 && strncmp(json + tok->start, "true", 4) == 0) { + *out = true; + return true; + } + if (len == 5 && strncmp(json + tok->start, "false", 5) == 0) { + *out = false; + return true; + } + return false; +} + +static bool json_token_to_raw(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) +{ + int len; + + if (!tok || !json || !out || outsz == 0) + return false; + len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) + return false; + memcpy(out, json + tok->start, (size_t)len); + out[len] = '\0'; + return true; +} + +static bool json_parse_request(const char *input, size_t len, + const char *key, char *out, size_t outsz, + json_get_type type, void *num_out) +{ + jsmn_parser parser; + jsmntok_t tokens[EVERLIVE_JSON_MAX_TOKENS]; + const jsmntok_t *val; + int tok_count; + int val_idx; + + if (!input || !key || len >= EVERLIVE_JSON_MAX_INPUT) + return false; + + jsmn_init(&parser); + tok_count = jsmn_parse(&parser, input, len, tokens, + EVERLIVE_JSON_MAX_TOKENS); + if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) + return false; + + val_idx = json_token_find_key(tokens, tok_count, key, input); + if (val_idx < 0) + return false; + + val = &tokens[val_idx]; + switch (type) { + case JSON_GET_STRING: + return json_token_to_string(val, input, out, outsz); + case JSON_GET_LONG: + return json_token_to_long(val, input, (long *)num_out); + case JSON_GET_DOUBLE: + return json_token_to_double(val, input, (double *)num_out); + case JSON_GET_BOOL: + return json_token_to_bool(val, input, (bool *)num_out); + case JSON_GET_RAW: + return json_token_to_raw(val, input, out, outsz); + } + return false; +} + +bool everlive_json_get_string(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + return json_parse_request(json, strlen(json), key, out, outsz, + JSON_GET_STRING, NULL); +} + +bool everlive_json_get_long(const char *json, const char *key, long *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_LONG, out); +} + +bool everlive_json_get_double(const char *json, const char *key, double *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_DOUBLE, out); +} + +bool everlive_json_get_bool(const char *json, const char *key, bool *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_BOOL, out); +} + +bool everlive_json_get_raw(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + return json_parse_request(json, strlen(json), key, out, outsz, + JSON_GET_RAW, NULL); +} + +bool everlive_json_find_value(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + + /* "method" is a string; everything else keeps its raw token form. */ + if (strcmp(key, "method") == 0) + return everlive_json_get_string(json, key, out, outsz); + + return everlive_json_get_raw(json, key, out, outsz); +} + +/* Quote-then-colon scan used only when jsmn cannot fit the document. */ +static bool json_scan_key(const char *json, const char *key) +{ + const size_t klen = strlen(key); + const char *p = json; + + while ((p = strstr(p, "\"")) != NULL) { + const char *q; + + p++; + if (strncmp(p, key, klen) != 0 || p[klen] != '"') + continue; + q = p + klen + 1; + while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') + q++; + if (*q == ':') + return true; + } + + return false; +} + +bool everlive_json_has_top_level_key(const char *json, const char *key) +{ + jsmn_parser parser; + jsmntok_t tokens[EVERLIVE_JSON_RESPONSE_TOKENS]; + int tok_count; + + if (!json || !key || !*key) + return false; + + jsmn_init(&parser); + tok_count = jsmn_parse(&parser, json, strlen(json), tokens, + EVERLIVE_JSON_RESPONSE_TOKENS); + if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) + return json_scan_key(json, key); + + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == strlen(key) && + memcmp(json + tokens[i].start, key, len) == 0) + return true; + } + + return false; +} + +void everlive_json_escape_into(char *dst, size_t dstsz, const char *src) +{ + const unsigned char *p; + size_t o = 0; + + if (!dst || dstsz == 0) + return; + + if (!src) { + dst[0] = '\0'; + return; + } + + for (p = (const unsigned char *)src; *p; p++) { + const char *esc = NULL; + + switch (*p) { + case '"': + esc = "\\\""; + break; + case '\\': + esc = "\\\\"; + break; + case '\n': + esc = "\\n"; + break; + case '\r': + esc = "\\r"; + break; + case '\t': + esc = "\\t"; + break; + default: + break; + } + + if (esc) { + if (o + 2 >= dstsz) + break; + dst[o++] = esc[0]; + dst[o++] = esc[1]; + } else if (*p < 0x20) { + char ubuf[7]; + + if (o + 6 >= dstsz) + break; + snprintf(ubuf, sizeof(ubuf), "\\u%04x", *p); + memcpy(dst + o, ubuf, 6); + o += 6; + } else { + if (o + 1 >= dstsz) + break; + dst[o++] = (char)*p; + } + } + + dst[o] = '\0'; +} + +/* Canonical JSON integer: -?(0|[1-9][0-9]*). */ +static bool json_is_canonical_int(const char *s) +{ + if (!s || !*s) + return false; + if (*s == '-') + s++; + if (!*s) + return false; + if (*s == '0') + return s[1] == '\0'; + if (*s < '1' || *s > '9') + return false; + for (s++; *s; s++) { + if (*s < '0' || *s > '9') + return false; + } + return true; +} + +void everlive_json_format_id(char *dst, size_t dstsz, const char *id) +{ + char inner[EVERLIVE_JSON_ID_MAX]; + const char *body = id; + size_t len; + size_t elen; + + if (!dst || dstsz == 0) + return; + + if (!id || !*id || strcmp(id, "null") == 0) { + snprintf(dst, dstsz, "null"); + return; + } + + if (json_is_canonical_int(id)) { + snprintf(dst, dstsz, "%s", id); + return; + } + + if (dstsz < 3) { + snprintf(dst, dstsz, "null"); + return; + } + + len = strlen(id); + if (id[0] == '"' && len >= 2 && id[len - 1] == '"') { + size_t in_len = len - 2; + + if (in_len >= sizeof(inner)) + in_len = sizeof(inner) - 1; + memcpy(inner, id + 1, in_len); + inner[in_len] = '\0'; + body = inner; + } + + dst[0] = '"'; + everlive_json_escape_into(dst + 1, dstsz - 2, body); + elen = strlen(dst); + dst[elen] = '"'; + dst[elen + 1] = '\0'; +} diff --git a/tools/libeverlive-core/everlive_json.h b/tools/libeverlive-core/everlive_json.h new file mode 100644 index 0000000..c5e8358 --- /dev/null +++ b/tools/libeverlive-core/everlive_json.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_json.h — Flat-object JSON-RPC 2.0 helpers for EverLive + * + * Stateless, allocation-free scanners built on the vendored jsmn parser. + * Every function is re-entrant: all scratch space lives on the caller's + * stack, so the GC573 control thread and the GC570D daemon loop may call + * these while the render/DMA paths are live. + */ + +#ifndef EVERLIVE_JSON_H +#define EVERLIVE_JSON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Request payloads are small. Responses (status.get) are ~135 tokens today; + * a 128-token budget returns JSMN_ERROR_NOMEM, so responses get 512. + */ +#define EVERLIVE_JSON_MAX_TOKENS 256U +#define EVERLIVE_JSON_RESPONSE_TOKENS 512U +#define EVERLIVE_JSON_MAX_INPUT (64U * 1024U) +#define EVERLIVE_JSON_ID_MAX 48U + +/* + * Typed getters. Return false when the key is absent, the document is + * malformed, or the value type does not match. + */ +bool everlive_json_get_string(const char *json, const char *key, + char *out, size_t outsz); +bool everlive_json_get_long(const char *json, const char *key, long *out); +bool everlive_json_get_double(const char *json, const char *key, double *out); +bool everlive_json_get_bool(const char *json, const char *key, bool *out); +bool everlive_json_get_raw(const char *json, const char *key, + char *out, size_t outsz); + +/* + * Compatibility helper used by the IPC dispatchers: "method" is extracted as + * an unquoted string, every other key as its raw token (number, null, or the + * unquoted contents of a string). Preserves `id` echo semantics. + */ +bool everlive_json_find_value(const char *json, const char *key, + char *out, size_t outsz); + +/* + * True when the root object has @key as a direct child. Replaces substring + * scanning for JSON-RPC error detection. Falls back to a quote+colon scan + * when the document cannot be parsed (e.g. a truncated reply). + */ +bool everlive_json_has_top_level_key(const char *json, const char *key); + +/* + * Escape @src as the body of a JSON string literal. Always NUL-terminates + * and never writes past dstsz-1. dst == NULL or dstsz == 0 is a no-op; + * src == NULL writes an empty string. + */ +void everlive_json_escape_into(char *dst, size_t dstsz, const char *src); + +/* + * Normalise a JSON-RPC "id" token for echoing: NULL/empty/"null" -> null, + * canonical integers are emitted verbatim, everything else is quoted and + * re-escaped. dstsz < 3 yields "null". + */ +void everlive_json_format_id(char *dst, size_t dstsz, const char *id); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_JSON_H */ diff --git a/tools/opengc573-userd/third_party/jsmn.c b/tools/libeverlive-core/third_party/jsmn.c similarity index 100% rename from tools/opengc573-userd/third_party/jsmn.c rename to tools/libeverlive-core/third_party/jsmn.c diff --git a/tools/opengc573-userd/third_party/jsmn.h b/tools/libeverlive-core/third_party/jsmn.h similarity index 100% rename from tools/opengc573-userd/third_party/jsmn.h rename to tools/libeverlive-core/third_party/jsmn.h diff --git a/tools/opengc570d-userd/Makefile b/tools/opengc570d-userd/Makefile index 1ea3959..ec732a1 100644 --- a/tools/opengc570d-userd/Makefile +++ b/tools/opengc570d-userd/Makefile @@ -9,11 +9,15 @@ CC ?= gcc PKG_CONFIG ?= pkg-config CFLAGS ?= -O2 -g -Wall -Wextra -pthread LDFLAGS ?= -pthread +DEPFLAGS := -MMD -MP + +CORE_DIR := ../libeverlive-core +CORE_LIB := $(CORE_DIR)/libeverlive_core.a USER_CFLAGS := -O2 -g -Wall -Wextra -Wno-unused-parameter \ -Wno-sign-compare -std=gnu11 \ -D_GNU_SOURCE -DGC570D_USERSPACE -DGC570D_DEVELOPER_DEBUGFS \ - -Isrc -I. + -Isrc -I. -I$(CORE_DIR) USER_LDFLAGS := -pthread -lm -lrt SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 2>/dev/null) @@ -43,7 +47,10 @@ USERD_SRCS := \ src/gc570d-dma.c \ src/gc570d-audio.c \ src/gc570d-video.c \ - src/gc570d_everlive.c + src/gc570d_everlive.c \ + src/gc570d-jsonrpc.c + +USERD_OBJS := $(USERD_SRCS:.c=.o) USER_HEADERS := \ src/gc570d.h \ @@ -51,6 +58,7 @@ USER_HEADERS := \ src/gc570d-user-compat.h \ src/gc570d-shm.h \ src/gc570d_everlive.h \ + src/gc570d-jsonrpc.h \ src/gc570d_font8x8.h \ data/gc570d_hdr_lut_3000.inc \ data/gc570d_no_signal_640x360.inc @@ -59,10 +67,16 @@ TARGETS := opengc570d-userd gc570d-ctl libgc570d-v4l2.so all: $(TARGETS) -opengc570d-userd: $(USERD_SRCS) $(USER_HEADERS) - $(CC) $(USER_CFLAGS) -o $@ $(USERD_SRCS) $(USER_LDFLAGS) $(LDFLAGS) +opengc570d-userd: $(USERD_OBJS) $(CORE_LIB) + $(CC) $(USER_CFLAGS) -o $@ $(USERD_OBJS) $(USER_LDFLAGS) $(LDFLAGS) $(CORE_LIB) @chmod 0755 $@ +$(CORE_LIB): + $(MAKE) -C $(CORE_DIR) + +src/%.o: src/%.c $(USER_HEADERS) + $(CC) $(USER_CFLAGS) $(DEPFLAGS) -c -o $@ $< + gc570d-ctl: src/gc570d-ctl.c $(CC) -O2 -g -Wall -Wextra -std=gnu11 -D_GNU_SOURCE -o $@ $< @chmod 0755 $@ @@ -75,6 +89,8 @@ libgc570d-v4l2.so: src/gc570d-v4l2-preload.c src/gc570d-shm.h test selftest: $(MAKE) -C ../../tests test +-include $(USERD_OBJS:.o=.d) + PREFIX ?= /usr/local DESTDIR ?= BINDIR := $(DESTDIR)$(PREFIX)/bin @@ -113,6 +129,6 @@ uninstall: rm -f $(DESTDIR)/etc/gc570d.conf.example clean: - rm -f $(TARGETS) + rm -f $(USERD_OBJS) $(USERD_OBJS:.o=.d) $(TARGETS) .PHONY: all clean test selftest install uninstall diff --git a/tools/opengc570d-userd/src/gc570d-jsonrpc.c b/tools/opengc570d-userd/src/gc570d-jsonrpc.c new file mode 100644 index 0000000..c2a9165 --- /dev/null +++ b/tools/opengc570d-userd/src/gc570d-jsonrpc.c @@ -0,0 +1,956 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * gc570d-jsonrpc.c — Unified JSON-RPC 2.0 control socket for the GC570D + * + * Transport: newline-delimited JSON-RPC 2.0 over AF_UNIX, exactly like the + * GC573 streamer. The daemon loop polls the listener and every connected + * client, then calls gc570d_jsonrpc_service() to drain request lines. No + * extra thread is used, so hardware access stays serialized with the + * watchdog and re-initialization paths. + */ + +#include "gc570d-jsonrpc.h" + +#include "gc570d.h" +#include "gc570d_everlive.h" +#include "everlive_json.h" +#include "everlive_ipc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RPC_REPLY_MAX 8192 +#define RPC_RESULT_MAX 6144 +#define RPC_METHOD_MAX 48 + +static int g_listen_fd = -1; +static int g_lock_fd = -1; +static bool g_bound; +static char g_path[EVERLIVE_IPC_PATH_MAX]; +static char g_lock_path[EVERLIVE_IPC_PATH_MAX + 8]; +static char g_version[24] = "unknown"; +static struct gc570d_dev *g_dev; +static void (*g_stats_fn)(struct gc570d_jsonrpc_stats *out); + +/* Per-input FPS estimation from the monotonic frame counters. */ +static uint64_t g_last_seq[2]; +static uint64_t g_last_fps_ns[2]; +static double g_fps[2]; + +/* Last LED colour applied through this interface. */ +static bool g_led_valid; +static unsigned int g_led_r; +static unsigned int g_led_g; +static unsigned int g_led_b; + +static uint64_t rpc_now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static void rpc_write_all(int fd, const char *data, size_t len) +{ + while (len > 0) { + ssize_t n = send(fd, data, len, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) + continue; + return; + } + data += n; + len -= (size_t)n; + } +} + +static void rpc_reply(int fd, const char *id, const char *result_json) +{ + char esc_id[EVERLIVE_JSON_ID_MAX * 2]; + char buf[RPC_REPLY_MAX]; + int n; + + everlive_json_format_id(esc_id, sizeof(esc_id), id); + n = snprintf(buf, sizeof(buf), + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}\n", + esc_id, result_json); + if (n > 0 && (size_t)n < sizeof(buf)) + rpc_write_all(fd, buf, (size_t)n); +} + +static void rpc_error(int fd, const char *id, int code, const char *message) +{ + char esc_id[EVERLIVE_JSON_ID_MAX * 2]; + char esc_msg[256]; + char buf[512]; + int n; + + everlive_json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_escape_into(esc_msg, sizeof(esc_msg), + message ? message : "error"); + n = snprintf(buf, sizeof(buf), + "{\"jsonrpc\":\"2.0\",\"id\":%s," + "\"error\":{\"code\":%d,\"message\":\"%s\"}}\n", + esc_id, code, esc_msg); + if (n > 0 && (size_t)n < sizeof(buf)) + rpc_write_all(fd, buf, (size_t)n); +} + +/* ------------------------------------------------------------------ */ +/* status.get */ +/* ------------------------------------------------------------------ */ + +static double rpc_port_fps(int port, uint64_t seq, uint64_t now) +{ + if (g_last_fps_ns[port] && now > g_last_fps_ns[port] && + seq >= g_last_seq[port]) { + g_fps[port] = (double)(seq - g_last_seq[port]) * 1000000000.0 / + (double)(now - g_last_fps_ns[port]); + } + g_last_seq[port] = seq; + g_last_fps_ns[port] = now; + return g_fps[port]; +} + +static void rpc_status(int fd, const char *id) +{ + struct gc570d_jsonrpc_stats st; + struct gc570d_dev *gc = g_dev; + char result[RPC_RESULT_MAX]; + char path_esc[96]; + uint64_t now = rpc_now_ns(); + int port = gc570d_everlive_port(); + unsigned int src_w = 0; + unsigned int src_h = 0; + unsigned int out_w = 0; + unsigned int out_h = 0; + unsigned int rate = 0; + unsigned int audio_rate = 48000; + unsigned int audio_ch = 2; + unsigned int led_r = 0; + unsigned int led_g = 0; + unsigned int led_b = 0; + bool streaming = false; + bool no_signal = true; + bool interlaced = false; + double fps; + int n; + + if (port < 0 || port > 1) + port = 0; + + memset(&st, 0, sizeof(st)); + if (g_stats_fn) + g_stats_fn(&st); + + if (gc) { + if (port == 0) { + streaming = READ_ONCE(gc->video0_streaming); + no_signal = READ_ONCE(gc->video0_no_signal); + interlaced = READ_ONCE(gc->video0_input_interlaced); + src_w = READ_ONCE(gc->video0_source_width); + src_h = READ_ONCE(gc->video0_source_height); + rate = READ_ONCE(gc->video0_source_fps); + out_w = READ_ONCE(gc->video0_width); + out_h = READ_ONCE(gc->video0_height); + audio_rate = READ_ONCE(gc->audio0_rate); + audio_ch = READ_ONCE(gc->audio0_channels); + led_r = gc->led_subleds[0].brightness; + led_g = gc->led_subleds[1].brightness; + led_b = gc->led_subleds[2].brightness; + } else { + streaming = READ_ONCE(gc->video_streaming); + no_signal = READ_ONCE(gc->video_no_signal); + interlaced = READ_ONCE(gc->video_input_interlaced); + src_w = READ_ONCE(gc->video_source_width); + src_h = READ_ONCE(gc->video_source_height); + rate = READ_ONCE(gc->video_source_fps); + out_w = READ_ONCE(gc->video_width); + out_h = READ_ONCE(gc->video_height); + audio_rate = READ_ONCE(gc->audio_rate); + audio_ch = READ_ONCE(gc->audio_channels); + led_r = gc->led_subleds[0].brightness; + led_g = gc->led_subleds[1].brightness; + led_b = gc->led_subleds[2].brightness; + } + } + + if (g_led_valid) { + led_r = g_led_r; + led_g = g_led_g; + led_b = g_led_b; + } + + if (!src_w) + src_w = out_w ? out_w : GC570D_WIDTH; + if (!src_h) + src_h = out_h ? out_h : GC570D_HEIGHT; + if (!out_w) + out_w = src_w; + if (!out_h) + out_h = src_h; + if (!rate) + rate = 60; + if (!audio_rate) + audio_rate = 48000; + if (!audio_ch) + audio_ch = 2; + + fps = rpc_port_fps(port, st.video_seq[port], now); + everlive_json_escape_into(path_esc, sizeof(path_esc), + gc570d_everlive_v4l2(port)); + + n = snprintf(result, sizeof(result), + "{\"proto\":1,\"version\":\"%s\",\"pid\":%ld," + "\"headless\":%s,\"streaming\":%s," + "\"signal\":{\"locked\":%s,\"width\":%u,\"height\":%u," + "\"interlaced\":%s,\"rate_hz\":%.2f," + "\"pixel_clock_khz\":0}," + "\"video\":{\"fps\":%.2f,\"frames\":%llu," + "\"ring_buffers\":4,\"ring_channels\":4," + "\"output_width\":%u,\"output_height\":%u}," + "\"audio\":{\"state\":\"lpcm\",\"label\":\"L-PCM 48kHz\"," + "\"rate_hz\":%u,\"channels\":%u,\"out_channels\":%u," + "\"layout\":\"stereo\",\"bits\":16," + "\"gain\":%.3f,\"muted\":%s}," + "\"v4l2\":{\"path\":\"%s\",\"configured\":%s," + "\"width\":%u,\"height\":%u,\"frames\":%llu," + "\"dropped\":%llu}," + "\"osd\":{\"enabled\":%s,\"position\":\"%s\"}," + "\"aspect\":\"%s\",\"scale\":\"%s\"," + "\"vsync\":false,\"fullscreen\":false," + "\"framegen\":{\"mode\":\"off\",\"present_fps\":0.00," + "\"output_fps\":0.00,\"unique_fps\":0.00," + "\"raw_fps\":0.00,\"flow_scale\":1.00," + "\"perf_mode\":false,\"low_latency\":false," + "\"layer_found\":false,\"dll_found\":false," + "\"dll_path\":\"\",\"vulkan_active\":false," + "\"deduplicator_active\":false}," + "\"led\":{\"r\":%u,\"g\":%u,\"b\":%u}," + "\"clients\":1,\"age_ms\":0}", + g_version, (long)getpid(), + gc570d_everlive_headless() ? "true" : "false", + streaming ? "true" : "false", + (streaming && !no_signal) ? "true" : "false", + src_w, src_h, interlaced ? "true" : "false", + (double)rate, + fps, (unsigned long long)st.video_seq[port], + out_w, out_h, + audio_rate, audio_ch, audio_ch, + (double)gc570d_everlive_volume() / 100.0, + gc570d_everlive_muted() ? "true" : "false", + path_esc, st.v4l2_configured[port] ? "true" : "false", + out_w, out_h, + (unsigned long long)st.v4l2_frames[port], + (unsigned long long)st.v4l2_dropped[port], + gc570d_everlive_osd() ? "true" : "false", + gc570d_everlive_osd_pos(), + gc570d_everlive_aspect(), gc570d_everlive_scale(), + led_r, led_g, led_b); + + if (n > 0 && (size_t)n < sizeof(result)) + rpc_reply(fd, id, result); + else + rpc_error(fd, id, -32603, "status serialization failed"); +} + +/* ------------------------------------------------------------------ */ +/* Request handlers */ +/* ------------------------------------------------------------------ */ + +static void rpc_ok(int fd, const char *id) +{ + rpc_reply(fd, id, "{\"ok\":true}"); +} + +static int rpc_require_dev(int fd, const char *id) +{ + if (g_dev) + return 0; + rpc_error(fd, id, -32603, "capture hardware unavailable"); + return -1; +} + +static void rpc_hello(int fd, const char *id) +{ + char result[640]; + char path_esc[128]; + int n; + + everlive_json_escape_into(path_esc, sizeof(path_esc), g_path); + n = snprintf(result, sizeof(result), + "{\"proto\":1,\"app\":\"everlive\",\"version\":\"%s\"," + "\"pid\":%ld,\"socket\":\"%s\"," + "\"methods\":[\"hello\",\"ping\",\"status.get\"," + "\"led.set\",\"led.mode.set\",\"audio.volume.set\"," + "\"audio.mute.set\",\"audio.channels.set\"," + "\"osd.set\",\"osd.position.set\",\"aspect.set\"," + "\"scale.set\",\"input.set\",\"pip.set\"," + "\"hotplug\",\"edid.set\"]}", + g_version, (long)getpid(), path_esc); + if (n > 0 && (size_t)n < sizeof(result)) + rpc_reply(fd, id, result); + else + rpc_error(fd, id, -32603, "hello serialization failed"); +} + +static void rpc_led_set(int fd, const char *id, const char *line) +{ + long r, g, b; + int ret; + + if (rpc_require_dev(fd, id) < 0) + return; + if (!everlive_json_get_long(line, "r", &r) || + !everlive_json_get_long(line, "g", &g) || + !everlive_json_get_long(line, "b", &b)) { + rpc_error(fd, id, -32602, "invalid params: r, g, b required"); + return; + } + + r = clamp_t(long, r, 0L, 255L); + g = clamp_t(long, g, 0L, 255L); + b = clamp_t(long, b, 0L, 255L); + + ret = gc570d_led_set_rgb(g_dev, (u8)r, (u8)g, (u8)b); + if (ret) { + rpc_error(fd, id, -32603, "failed to apply LED colour"); + return; + } + + g_led_r = (unsigned int)r; + g_led_g = (unsigned int)g; + g_led_b = (unsigned int)b; + g_led_valid = true; + rpc_ok(fd, id); +} + +static void rpc_led_mode(int fd, const char *id, const char *line) +{ + char mode[24]; + const char *effect; + int ret; + + if (rpc_require_dev(fd, id) < 0) + return; + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + + if (!strcasecmp(mode, "off") || !strcasecmp(mode, "dark")) + effect = "off"; + else if (!strcasecmp(mode, "static") || !strcasecmp(mode, "solid")) + effect = "static"; + else if (!strcasecmp(mode, "breathing") || !strcasecmp(mode, "breathe") || + !strcasecmp(mode, "wave")) + effect = "breathing"; + else if (!strcasecmp(mode, "colorful") || !strcasecmp(mode, "rainbow") || + !strcasecmp(mode, "cycle")) + effect = "colorful"; + else { + rpc_error(fd, id, -32602, + "unknown mode: off|static|breathing|colorful|wave|rainbow"); + return; + } + + ret = gc570d_led_set_effect(g_dev, effect); + if (ret) { + rpc_error(fd, id, -32603, "failed to apply LED effect"); + return; + } + g_led_valid = false; + rpc_ok(fd, id); +} + +static void rpc_audio_volume(int fd, const char *id, const char *line) +{ + double gain; + int pct; + + if (!everlive_json_get_double(line, "gain", &gain)) { + rpc_error(fd, id, -32602, "invalid params: gain required"); + return; + } + if (gain < 0.0) + gain = 0.0; + if (gain > 1.5) + gain = 1.5; + + pct = (int)(gain * 100.0 + 0.5); + gc570d_everlive_set_volume(pct); + rpc_ok(fd, id); +} + +static void rpc_audio_mute(int fd, const char *id, const char *line) +{ + bool muted; + + if (!everlive_json_get_bool(line, "muted", &muted)) { + rpc_error(fd, id, -32602, "invalid params: muted required"); + return; + } + gc570d_everlive_set_mute(muted); + rpc_ok(fd, id); +} + +static void rpc_audio_channels(int fd, const char *id, const char *line) +{ + char mode[16]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "auto") && strcasecmp(mode, "stereo") && + strcasecmp(mode, "2") && strcasecmp(mode, "2.0")) { + rpc_error(fd, id, -32602, + "GC570D captures LPCM stereo only (auto|stereo)"); + return; + } + rpc_ok(fd, id); +} + +static void rpc_osd(int fd, const char *id, const char *line) +{ + bool enabled; + + if (!everlive_json_get_bool(line, "enabled", &enabled)) { + rpc_error(fd, id, -32602, "invalid params: enabled required"); + return; + } + gc570d_everlive_set_osd(enabled); + rpc_ok(fd, id); +} + +static void rpc_osd_pos(int fd, const char *id, const char *line) +{ + char pos[16]; + + if (!everlive_json_get_string(line, "pos", pos, sizeof(pos))) { + rpc_error(fd, id, -32602, "invalid params: pos required"); + return; + } + if (strcasecmp(pos, "tr") && strcasecmp(pos, "br") && + strcasecmp(pos, "bl") && strcasecmp(pos, "tl") && + strcasecmp(pos, "top-right") && strcasecmp(pos, "bottom-right") && + strcasecmp(pos, "bottom-left") && strcasecmp(pos, "top-left")) { + rpc_error(fd, id, -32602, "unknown pos: tr|br|bl|tl"); + return; + } + gc570d_everlive_set_osd_pos(pos); + rpc_ok(fd, id); +} + +static void rpc_aspect(int fd, const char *id, const char *line) +{ + char mode[24]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "auto") && strcasecmp(mode, "16:9") && + strcasecmp(mode, "16/9") && strcasecmp(mode, "4:3") && + strcasecmp(mode, "4/3") && strcasecmp(mode, "stretch") && + strcasecmp(mode, "fill")) { + rpc_error(fd, id, -32602, + "unknown mode: auto|16:9|4:3|stretch"); + return; + } + gc570d_everlive_set_aspect(mode); + rpc_ok(fd, id); +} + +static void rpc_scale(int fd, const char *id, const char *line) +{ + char mode[16]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "fit") && strcasecmp(mode, "aspect") && + strcasecmp(mode, "integer") && strcasecmp(mode, "nearest")) { + rpc_error(fd, id, -32602, "unknown mode: fit|integer"); + return; + } + gc570d_everlive_set_scale(mode); + rpc_ok(fd, id); +} + +static void rpc_input(int fd, const char *id, const char *line) +{ + long port; + + if (!everlive_json_get_long(line, "port", &port)) { + rpc_error(fd, id, -32602, "invalid params: port required"); + return; + } + /* + * Canonical values are 1-based (matching gc570d-ctl): 1 = HDMI 1, + * 2 = HDMI 2. 0 is also accepted as HDMI 1 for 0-based clients. + */ + if (port < 0 || port > 2) { + rpc_error(fd, id, -32602, "port must be 1 or 2"); + return; + } + gc570d_everlive_set_port(port == 2 ? 1 : 0); + rpc_ok(fd, id); +} + +static void rpc_pip(int fd, const char *id, const char *line) +{ + bool enabled; + + if (!everlive_json_get_bool(line, "enabled", &enabled)) { + rpc_error(fd, id, -32602, "invalid params: enabled required"); + return; + } + gc570d_everlive_set_pip(enabled); + rpc_ok(fd, id); +} + +static void rpc_hotplug(int fd, const char *id, const char *line) +{ + long port = 0; + const char *node; + int ret; + + if (!everlive_json_get_long(line, "port", &port)) + port = 1; + if (port < 0 || port > 2) { + rpc_error(fd, id, -32602, "port must be 1 or 2"); + return; + } + + node = (port == 2) ? "it6802_hpd" : "it68051_hpd"; + ret = gc570d_user_diag_action(node, "1"); + if (ret < 0) { + rpc_error(fd, id, -32603, "HPD pulse failed"); + return; + } + rpc_ok(fd, id); +} + +static void rpc_edid(int fd, const char *id, const char *line) +{ + char profile[32]; + const char *node; + int ret; + + if (!everlive_json_get_string(line, "profile", profile, + sizeof(profile))) { + rpc_error(fd, id, -32602, "invalid params: profile required"); + return; + } + + if (!strcasecmp(profile, "default") || !strcasecmp(profile, "bridge") || + !strcasecmp(profile, "internal")) + node = "splitter_windows_bridge_edid"; + else if (!strcasecmp(profile, "copy") || + !strcasecmp(profile, "passthrough") || + !strcasecmp(profile, "downstream")) + node = "splitter_windows_edid_copy"; + else { + rpc_error(fd, id, -32602, + "unknown profile: default|bridge|copy|passthrough"); + return; + } + + ret = gc570d_user_diag_action(node, "1"); + if (ret < 0) { + rpc_error(fd, id, -32603, "EDID switch failed"); + return; + } + rpc_ok(fd, id); +} + +static bool rpc_is_unsupported(const char *method) +{ + static const char *const unsupported[] = { + "mode.set", "v4l2.set", "hdr.set", "hdr.passthrough", + "hdr.exposure", "framegen.set", "toast.show", + }; + + for (size_t i = 0; i < ARRAY_SIZE(unsupported); i++) { + if (strcmp(method, unsupported[i]) == 0) + return true; + } + return false; +} + +static void rpc_dispatch(int fd, const char *id, const char *method, + const char *line) +{ + if (strcmp(method, "hello") == 0) + rpc_hello(fd, id); + else if (strcmp(method, "ping") == 0) + rpc_reply(fd, id, "\"pong\""); + else if (strcmp(method, "status.get") == 0) + rpc_status(fd, id); + else if (strcmp(method, "led.set") == 0) + rpc_led_set(fd, id, line); + else if (strcmp(method, "led.mode.set") == 0) + rpc_led_mode(fd, id, line); + else if (strcmp(method, "audio.volume.set") == 0) + rpc_audio_volume(fd, id, line); + else if (strcmp(method, "audio.mute.set") == 0) + rpc_audio_mute(fd, id, line); + else if (strcmp(method, "audio.channels.set") == 0) + rpc_audio_channels(fd, id, line); + else if (strcmp(method, "osd.set") == 0) + rpc_osd(fd, id, line); + else if (strcmp(method, "osd.position.set") == 0) + rpc_osd_pos(fd, id, line); + else if (strcmp(method, "aspect.set") == 0) + rpc_aspect(fd, id, line); + else if (strcmp(method, "scale.set") == 0) + rpc_scale(fd, id, line); + else if (strcmp(method, "input.set") == 0) + rpc_input(fd, id, line); + else if (strcmp(method, "pip.set") == 0) + rpc_pip(fd, id, line); + else if (strcmp(method, "hotplug") == 0) + rpc_hotplug(fd, id, line); + else if (strcmp(method, "edid.set") == 0) + rpc_edid(fd, id, line); + else if (rpc_is_unsupported(method)) + rpc_error(fd, id, -32601, + "method not supported on the GC570D engine"); + else + rpc_error(fd, id, -32601, "method not found"); +} + +static void rpc_handle_request(int fd, const char *line) +{ + char id[EVERLIVE_JSON_ID_MAX]; + char method[RPC_METHOD_MAX]; + bool has_id; + const char *rid; + + has_id = everlive_json_find_value(line, "id", id, sizeof(id)) && + strcmp(id, "null") != 0; + rid = has_id ? id : NULL; + + if (!everlive_json_get_string(line, "method", method, sizeof(method))) { + if (has_id) + rpc_error(fd, rid, -32700, + "parse error: missing method"); + return; + } + + rpc_dispatch(fd, rid, method, line); +} + +/* ------------------------------------------------------------------ */ +/* Transport */ +/* ------------------------------------------------------------------ */ + +#define RPC_CLIENT_IN_MAX 8192 + +struct rpc_client { + int fd; + size_t inlen; + bool active; + char in[RPC_CLIENT_IN_MAX]; +}; + +static struct rpc_client g_clients[GC570D_JSONRPC_MAX_CLIENTS]; + +static struct rpc_client *rpc_client_alloc(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) { + if (!g_clients[i].active) + return &g_clients[i]; + } + return NULL; +} + +static void rpc_client_close(struct rpc_client *c) +{ + if (!c->active) + return; + close(c->fd); + c->fd = -1; + c->active = false; + c->inlen = 0; +} + +static void rpc_client_close_all(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) + rpc_client_close(&g_clients[i]); +} + +static bool rpc_peer_ok(int fd) +{ + struct ucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) < 0) + return false; + return cred.uid == getuid(); +} + +static void rpc_drain_lines(struct rpc_client *c) +{ + size_t consumed = 0; + + while (consumed < c->inlen) { + char *line = c->in + consumed; + size_t avail = c->inlen - consumed; + char *nl = memchr(line, '\n', avail); + size_t line_len; + size_t total; + + if (!nl) + break; + line_len = (size_t)(nl - line); + total = line_len + 1; + *nl = '\0'; + if (line_len && line[line_len - 1] == '\r') + line[--line_len] = '\0'; + if (line_len) + rpc_handle_request(c->fd, line); + consumed += total; + if (!c->active) + return; + } + + if (consumed) { + memmove(c->in, c->in + consumed, c->inlen - consumed); + c->inlen -= consumed; + } + c->in[c->inlen] = '\0'; +} + +static void rpc_client_read(struct rpc_client *c) +{ + for (;;) { + size_t space = sizeof(c->in) - c->inlen - 1; + ssize_t n; + + if (space == 0) { + rpc_error(c->fd, NULL, -32000, "frame too large"); + rpc_client_close(c); + return; + } + + n = recv(c->fd, c->in + c->inlen, space, MSG_DONTWAIT); + if (n == 0) { + rpc_client_close(c); + return; + } + if (n < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + rpc_client_close(c); + return; + } + + c->inlen += (size_t)n; + c->in[c->inlen] = '\0'; + rpc_drain_lines(c); + if (!c->active) + return; + } +} + +int gc570d_jsonrpc_fill_pollfds(struct pollfd *fds, int max) +{ + int n = 0; + + if (!fds || max <= 0) + return 0; + + if (g_listen_fd >= 0 && n < max) { + fds[n].fd = g_listen_fd; + fds[n].events = POLLIN; + n++; + } + + for (size_t i = 0; i < ARRAY_SIZE(g_clients) && n < max; i++) { + if (!g_clients[i].active) + continue; + fds[n].fd = g_clients[i].fd; + fds[n].events = POLLIN; + n++; + } + + return n; +} + +void gc570d_jsonrpc_service(void) +{ + struct pollfd pfd[1 + GC570D_JSONRPC_MAX_CLIENTS]; + int map[1 + GC570D_JSONRPC_MAX_CLIENTS]; + int n = 0; + + if (g_listen_fd >= 0) { + pfd[n].fd = g_listen_fd; + pfd[n].events = POLLIN; + map[n] = -1; + n++; + } + + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) { + if (!g_clients[i].active) + continue; + pfd[n].fd = g_clients[i].fd; + pfd[n].events = POLLIN; + map[n] = (int)i; + n++; + } + + if (n == 0) + return; + if (poll(pfd, (nfds_t)n, 0) <= 0) + return; + + for (int i = 0; i < n; i++) { + if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR))) + continue; + + if (map[i] < 0) { + for (;;) { + struct rpc_client *c; + int cfd = accept4(g_listen_fd, NULL, NULL, + SOCK_NONBLOCK | SOCK_CLOEXEC); + + if (cfd < 0) { + if (errno == EINTR) + continue; + break; + } + c = rpc_client_alloc(); + if (!c || !rpc_peer_ok(cfd)) { + close(cfd); + continue; + } + c->fd = cfd; + c->inlen = 0; + c->in[0] = '\0'; + c->active = true; + } + } else { + rpc_client_read(&g_clients[map[i]]); + } + } +} + +int gc570d_jsonrpc_init(const char *version) +{ + struct sockaddr_un addr; + int ret; + + if (g_listen_fd >= 0) + return 0; + + snprintf(g_version, sizeof(g_version), "%s", + version && *version ? version : "unknown"); + + ret = everlive_ipc_prepare_socket_path(g_path, sizeof(g_path), NULL); + if (ret < 0) + return ret; + + snprintf(g_lock_path, sizeof(g_lock_path), "%.*s.lock", + EVERLIVE_IPC_PATH_MAX - 1, g_path); + + g_lock_fd = open(g_lock_path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + if (g_lock_fd < 0) { + ret = -errno; + goto fail; + } + if (flock(g_lock_fd, LOCK_EX | LOCK_NB) < 0) { + ret = (errno == EWOULDBLOCK) ? -EADDRINUSE : -errno; + goto fail; + } + + /* A leftover socket without a live lock owner is stale. */ + if (access(g_path, F_OK) == 0) + (void)unlink(g_path); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, g_path, strlen(g_path) + 1); + + g_listen_fd = socket(AF_UNIX, + SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (g_listen_fd < 0) { + ret = -errno; + goto fail; + } + if (bind(g_listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + ret = -errno; + goto fail; + } + g_bound = true; + (void)chmod(g_path, 0600); + if (listen(g_listen_fd, GC570D_JSONRPC_MAX_CLIENTS) < 0) { + ret = -errno; + goto fail; + } + + return 0; + +fail: + gc570d_jsonrpc_shutdown(); + return ret; +} + +void gc570d_jsonrpc_shutdown(void) +{ + rpc_client_close_all(); + + if (g_listen_fd >= 0) { + close(g_listen_fd); + g_listen_fd = -1; + } + if (g_path[0] && g_bound) + (void)unlink(g_path); + g_path[0] = '\0'; + g_bound = false; + if (g_lock_fd >= 0) { + (void)flock(g_lock_fd, LOCK_UN); + close(g_lock_fd); + g_lock_fd = -1; + } + g_lock_path[0] = '\0'; +} + +int gc570d_jsonrpc_listen_fd(void) +{ + return g_listen_fd; +} + +const char *gc570d_jsonrpc_socket_path(void) +{ + return g_path; +} + +void gc570d_jsonrpc_set_stats_fn(void (*fn)(struct gc570d_jsonrpc_stats *out)) +{ + g_stats_fn = fn; +} + +void gc570d_jsonrpc_set_dev(struct gc570d_dev *gc) +{ + g_dev = gc; +} diff --git a/tools/opengc570d-userd/src/gc570d-jsonrpc.h b/tools/opengc570d-userd/src/gc570d-jsonrpc.h new file mode 100644 index 0000000..7746bcc --- /dev/null +++ b/tools/opengc570d-userd/src/gc570d-jsonrpc.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc570d-jsonrpc.h — Unified JSON-RPC 2.0 control socket for the GC570D + * + * The daemon keeps serving its legacy plain-text command socket for + * gc570d-ctl, and additionally binds the shared everlive.sock so EverLive's + * CLI and desktop hub can talk to both capture cards through one protocol. + */ +#ifndef GC570D_JSONRPC_H +#define GC570D_JSONRPC_H + +#include +#include +#include +#include + +struct gc570d_dev; + +#define GC570D_JSONRPC_MAX_CLIENTS 8 + +struct gc570d_jsonrpc_stats { + uint64_t video_seq[2]; + uint64_t audio_periods[2]; + uint64_t irq_dma0; + uint64_t irq_dma1; + bool v4l2_configured[2]; + uint64_t v4l2_frames[2]; + uint64_t v4l2_dropped[2]; +}; + +/* + * Bind the unified runtime socket (respects $EVERLIVE_SOCKET and the legacy + * $EVERLIVE_CONTROL_SOCKET; otherwise $XDG_RUNTIME_DIR/everlive.sock). + * Returns 0 on success or a negative errno. + */ +int gc570d_jsonrpc_init(const char *version); + +/* Close the listener and unlink the socket + lock file. Idempotent. */ +void gc570d_jsonrpc_shutdown(void); + +/* For the daemon poll() loop; -1 when not initialised. */ +int gc570d_jsonrpc_listen_fd(void); +/* Path of the bound socket, or "" when not running. */ +const char *gc570d_jsonrpc_socket_path(void); + +/* + * Add the listener and every connected client to @fds. Returns the number of + * descriptors written; the daemon poll() loop can then call + * gc570d_jsonrpc_service() when any of them reports activity. + */ +int gc570d_jsonrpc_fill_pollfds(struct pollfd *fds, int max); + +/* Accept new clients and drain every readable request line. Non-blocking. */ +void gc570d_jsonrpc_service(void); + +/* Bind the pipeline snapshot callback (implemented in gc570d-userspace.c). */ +void gc570d_jsonrpc_set_stats_fn(void (*fn)(struct gc570d_jsonrpc_stats *out)); + +/* Bind the active device pointer; re-arm after a hardware re-init. */ +void gc570d_jsonrpc_set_dev(struct gc570d_dev *gc); + +#endif /* GC570D_JSONRPC_H */ diff --git a/tools/opengc570d-userd/src/gc570d-led.c b/tools/opengc570d-userd/src/gc570d-led.c index 009c1a8..33d27db 100644 --- a/tools/opengc570d-userd/src/gc570d-led.c +++ b/tools/opengc570d-userd/src/gc570d-led.c @@ -308,63 +308,120 @@ static int gc570d_led_set_blocking(struct led_classdev *led_cdev, return ret; } -static ssize_t gc570d_led_effect_show(struct device *dev, - struct device_attribute *attr, char *buf) +/* + * Set a static RGB colour, cancelling any breathing/colorful animation. + * Public entry point for the JSON-RPC control plane. + */ +int gc570d_led_set_rgb(struct gc570d_dev *gc, u8 red, u8 green, u8 blue) { - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); - bool breathing; - bool colorful; + int ret; + + if (!gc) + return -EINVAL; + if (READ_ONCE(gc->led_removing)) + return -ENODEV; + + cancel_delayed_work_sync(&gc->led_work); mutex_lock(&gc->led_lock); - breathing = gc->led_breathing; - colorful = gc->led_colorful; - mutex_unlock(&gc->led_lock); + if (gc->led_removing) { + ret = -ENODEV; + goto out_unlock; + } - return sysfs_emit(buf, "%s\n", - colorful ? "colorful" : - breathing ? "breathing" : "static"); + gc->led_breathing = false; + gc->led_colorful = false; + gc->led_subleds[0].brightness = red; + gc->led_subleds[1].brightness = green; + gc->led_subleds[2].brightness = blue; + gc->led_subleds[0].intensity = red; + gc->led_subleds[1].intensity = green; + gc->led_subleds[2].intensity = blue; + if (red || green || blue) + gc->led_mcdev.led_cdev.brightness = GC570D_LED_MAX_BRIGHTNESS; + else + gc->led_mcdev.led_cdev.brightness = LED_OFF; + + ret = gc570d_led_hw_init(gc); + if (!ret) + ret = gc570d_led_hw_set_rgb(gc, red, green, blue); + +out_unlock: + mutex_unlock(&gc->led_lock); + return ret; } -static ssize_t gc570d_led_effect_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +/* + * Select an LED effect by name: "off", "static", "breathing", "colorful". + * The legacy spellings accepted by the sysfs attribute ("breathe", + * "colorful-breathing") and the hub's "rainbow" alias are handled too. + */ +int gc570d_led_set_effect(struct gc570d_dev *gc, const char *effect) { - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); bool breathing; bool colorful; + bool off = false; int ret; - if (sysfs_streq(buf, "colorful") || - sysfs_streq(buf, "colorful-breathing")) { + if (!gc || !effect) + return -EINVAL; + + if (strcmp(effect, "colorful") == 0 || + strcmp(effect, "colorful-breathing") == 0 || + strcmp(effect, "rainbow") == 0) { breathing = false; colorful = true; - } else if (sysfs_streq(buf, "breathing") || - sysfs_streq(buf, "breathe")) { + } else if (strcmp(effect, "breathing") == 0 || + strcmp(effect, "breathe") == 0) { breathing = true; colorful = false; - } else if (sysfs_streq(buf, "static")) { + } else if (strcmp(effect, "static") == 0) { + breathing = false; + colorful = false; + } else if (strcmp(effect, "off") == 0) { breathing = false; colorful = false; + off = true; } else { return -EINVAL; } - /* FIX: Check the removing flag before attempting to cancel work to prevent unregister deadlocks */ if (READ_ONCE(gc->led_removing)) return -ENODEV; cancel_delayed_work_sync(&gc->led_work); - + mutex_lock(&gc->led_lock); if (gc->led_removing) { ret = -ENODEV; goto out_unlock; } + if (off) { + gc->led_breathing = false; + gc->led_colorful = false; + gc->led_mcdev.led_cdev.brightness = LED_OFF; + ret = gc570d_led_shutdown(gc); + goto out_unlock; + } + + /* + * When switching from 'off' (or zero brightness) to an active effect, + * restore default brightness so breathing and colorful effects do not + * scale all color channels to zero. + */ + if (gc->led_mcdev.led_cdev.brightness == LED_OFF) + gc->led_mcdev.led_cdev.brightness = GC570D_LED_MAX_BRIGHTNESS; + + if (breathing && + gc->led_subleds[0].intensity == 0 && + gc->led_subleds[1].intensity == 0 && + gc->led_subleds[2].intensity == 0) { + gc->led_subleds[2].intensity = GC570D_LED_WINDOWS_BLUE; + } + ret = led_mc_calc_color_components(&gc->led_mcdev, - led_cdev->brightness); + gc->led_mcdev.led_cdev.brightness); if (!ret) ret = gc570d_led_hw_init(gc); if (ret) @@ -376,7 +433,7 @@ static ssize_t gc570d_led_effect_store(struct device *dev, gc->led_step = 1; gc->led_descending = false; gc->led_palette_index = 0; - /* FIX: Route slow blocking I2C operations to a dedicated system_unbound_wq */ + /* Route slow blocking I2C operations to a dedicated workqueue. */ mod_delayed_work(system_unbound_wq, &gc->led_work, 0); } else { ret = gc570d_led_hw_set_rgb(gc, @@ -387,7 +444,47 @@ static ssize_t gc570d_led_effect_store(struct device *dev, out_unlock: mutex_unlock(&gc->led_lock); - return ret ? ret : count; + return ret; +} + +static ssize_t gc570d_led_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); + bool breathing; + bool colorful; + + mutex_lock(&gc->led_lock); + breathing = gc->led_breathing; + colorful = gc->led_colorful; + mutex_unlock(&gc->led_lock); + + return sysfs_emit(buf, "%s\n", + colorful ? "colorful" : + breathing ? "breathing" : "static"); +} + +static ssize_t gc570d_led_effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); + char effect[24]; + size_t len = count; + int ret; + + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r' || + buf[len - 1] == ' ' || buf[len - 1] == '\t')) + len--; + if (len == 0 || len >= sizeof(effect)) + return -EINVAL; + memcpy(effect, buf, len); + effect[len] = '\0'; + + ret = gc570d_led_set_effect(gc, effect); + return ret ? ret : (ssize_t)count; } static DEVICE_ATTR(effect, 0644, gc570d_led_effect_show, diff --git a/tools/opengc570d-userd/src/gc570d-userspace.c b/tools/opengc570d-userd/src/gc570d-userspace.c index cf68fb0..7dff800 100644 --- a/tools/opengc570d-userd/src/gc570d-userspace.c +++ b/tools/opengc570d-userd/src/gc570d-userspace.c @@ -10,6 +10,12 @@ #include "gc570d.h" #include "gc570d-shm.h" #include "gc570d_everlive.h" +#include "everlive_card.h" +#include "gc570d-jsonrpc.h" + +#ifndef EVERLIVE_VERSION +#define EVERLIVE_VERSION "1.5.0" +#endif #include #include @@ -967,6 +973,29 @@ void debugfs_remove_recursive(struct dentry *dentry) g_diag_count = 0; } +int gc570d_user_diag_action(const char *name, const char *value) +{ + if (!name || !*name || !value) + return -EINVAL; + + for (size_t i = 0; i < g_diag_count; i++) { + struct file f; + loff_t pos = 0; + ssize_t r; + + if (strcmp(g_diag_entries[i].name, name) != 0 || + !g_diag_entries[i].fops->write) + continue; + memset(&f, 0, sizeof(f)); + f.private_data = g_diag_entries[i].data; + r = g_diag_entries[i].fops->write(&f, value, strlen(value), + &pos); + return (int)r; + } + + return -ENOENT; +} + int led_classdev_multicolor_register(struct device *parent, struct led_classdev_mc *mcled_cdev) { (void)parent; @@ -2297,6 +2326,28 @@ void snd_card_free(struct snd_card *card) free(card); } +/* + * Snapshot for the JSON-RPC status.get handler. Monotonic counters are read + * without the stream locks: they only ever advance, and a torn sample would + * at worst under-report a frame count for one poll interval. + */ +void gc570d_user_pipeline_stats(struct gc570d_jsonrpc_stats *out) +{ + if (!out) + return; + + memset(out, 0, sizeof(*out)); + for (int i = 0; i < 2; i++) { + out->video_seq[i] = g_vstreams[i].latest_seq; + out->audio_periods[i] = g_astreams[i].periods_published; + out->v4l2_configured[i] = gc570d_everlive_v4l2(i)[0] != '\0'; + } + if (g_gc) { + out->irq_dma0 = (uint64_t)atomic64_read(&g_gc->irq_dma0); + out->irq_dma1 = (uint64_t)atomic64_read(&g_gc->irq_dma1); + } +} + /* ========================================================================= * 5. UNIX Control & Diagnostic Socket (/run/user/$UID/gc570d/control.sock) * ========================================================================= */ @@ -2404,21 +2455,14 @@ static void gc570d_handle_ctl_client(int client_fd) } else if (strncmp(cmd, "action ", 7) == 0) { char name[64] = { 0 }; char val[64] = "1"; + int r; + sscanf(cmd + 7, "%63s %63s", name, val); - bool found = false; - for (size_t i = 0; i < g_diag_count; i++) { - if (strcmp(g_diag_entries[i].name, name) == 0 && - g_diag_entries[i].fops->write) { - struct file f = { .private_data = g_diag_entries[i].data }; - loff_t pos = 0; - ssize_t r = g_diag_entries[i].fops->write(&f, val, strlen(val), &pos); - fprintf(fp, "action %s(%s) -> %zd\n", name, val, r); - found = true; - break; - } - } - if (!found) + r = gc570d_user_diag_action(name, val); + if (r == -ENOENT) fprintf(fp, "Unknown or read-only action node: %s\n", name); + else + fprintf(fp, "action %s(%s) -> %d\n", name, val, r); } else if (strncmp(cmd, "led ", 4) == 0) { const char *mode = cmd + 4; if (strcmp(mode, "off") == 0 || strcmp(mode, "0") == 0) { @@ -2542,53 +2586,24 @@ static void gc570d_ctl_server_shutdown(void) * 6. VFIO-PCI Setup & Main Daemon Loop * ========================================================================= */ -static int read_sysfs_hex(const char *dir, const char *attr, uint32_t *out) -{ - char path[512], buf[64]; - snprintf(path, sizeof(path), "%s/%s", dir, attr); - FILE *fp = fopen(path, "r"); - if (!fp) - return -errno; - if (!fgets(buf, sizeof(buf), fp)) { - fclose(fp); - return -EIO; - } - fclose(fp); - *out = (uint32_t)strtoul(buf, NULL, 0); - return 0; -} - +/* + * Card discovery lives in libeverlive-core. 1461:0054 is shared with the + * GC573/GC555, so only a descriptor positively identified as "gc570d" + * (subsystem 1461:5700) is accepted. A missing subsystem file fails safe. + */ static int find_gc570d_pci_bdf(char *bdf_out, size_t bdf_size) { - DIR *d = opendir("/sys/bus/pci/devices"); - if (!d) - return -errno; - struct dirent *ent; - while ((ent = readdir(d)) != NULL) { - if (ent->d_name[0] == '.') + struct everlive_device cards[8]; + int count; + + count = everlive_detect_installed_cards(cards, ARRAY_SIZE(cards), NULL); + for (int i = 0; i < count; i++) { + if (strcmp(cards[i].desc->model_slug, "gc570d") != 0) continue; - char dir[512]; - uint32_t vendor = 0, device = 0, subvend = 0, subdev = 0; - snprintf(dir, sizeof(dir), "/sys/bus/pci/devices/%s", ent->d_name); - if (read_sysfs_hex(dir, "vendor", &vendor) == 0 && - read_sysfs_hex(dir, "device", &device) == 0 && - vendor == GC570D_VENDOR_ID && device == GC570D_DEVICE_ID) { - /* - * 1461:0054 is shared with other AVerMedia cards - * (GC573/GC555). Only the DUO exposes subsystem - * 1461:5700; skip anything we cannot positively identify. - */ - if (read_sysfs_hex(dir, "subsystem_vendor", &subvend) < 0 || - read_sysfs_hex(dir, "subsystem_device", &subdev) < 0 || - subvend != GC570D_SUBSYSTEM_VENDOR_ID || - subdev != GC570D_SUBSYSTEM_ID) - continue; - strscpy(bdf_out, ent->d_name, bdf_size); - closedir(d); - return 0; - } + strscpy(bdf_out, cards[i].pci_bdf, bdf_size); + return 0; } - closedir(d); + return -ENODEV; } @@ -3170,6 +3185,17 @@ int main(int argc, char **argv) "[gc570d WARN] Failed to start EverLive frontend: %d\n", ret); + gc570d_jsonrpc_set_stats_fn(gc570d_user_pipeline_stats); + gc570d_jsonrpc_set_dev(g_gc); + if (gc570d_jsonrpc_init(EVERLIVE_VERSION) < 0) { + fprintf(stderr, + "[gc570d WARN] JSON-RPC control socket unavailable; continuing without remote control\n"); + } else { + fprintf(stderr, + "[gc570d INFO] JSON-RPC control socket: %s\n", + gc570d_jsonrpc_socket_path()); + } + struct timespec ts_mono_prev = { 0 }, ts_boot_prev = { 0 }; clock_gettime(CLOCK_MONOTONIC, &ts_mono_prev); clock_gettime(CLOCK_BOOTTIME, &ts_boot_prev); @@ -3182,16 +3208,43 @@ int main(int argc, char **argv) while (!atomic_load(&g_daemon_quit) && !gc570d_everlive_quit_requested()) { - struct pollfd pfd = { - .fd = g_ctl_listen_fd, - .events = POLLIN, - }; - int pr = poll(&pfd, g_ctl_listen_fd >= 0 ? 1 : 0, 250); - if (pr > 0 && (pfd.revents & POLLIN)) { - int cfd = accept4(g_ctl_listen_fd, NULL, NULL, SOCK_CLOEXEC); - if (cfd >= 0) { - gc570d_handle_ctl_client(cfd); - close(cfd); + struct pollfd pfd[1 + GC570D_JSONRPC_MAX_CLIENTS + 1]; + int nfds = 0; + int rpc_base; + int pr; + + if (g_ctl_listen_fd >= 0) { + pfd[nfds].fd = g_ctl_listen_fd; + pfd[nfds].events = POLLIN; + nfds++; + } + rpc_base = nfds; + nfds += gc570d_jsonrpc_fill_pollfds(&pfd[nfds], + (int)(ARRAY_SIZE(pfd) - (size_t)nfds)); + + pr = poll(pfd, (nfds_t)nfds, 250); + + if (pr > 0) { + bool rpc_ready = false; + + for (int i = rpc_base; i < nfds; i++) { + if (pfd[i].revents) { + rpc_ready = true; + break; + } + } + if (rpc_ready) + gc570d_jsonrpc_service(); + + if (g_ctl_listen_fd >= 0 && + (pfd[0].revents & POLLIN)) { + int cfd = accept4(g_ctl_listen_fd, NULL, NULL, + SOCK_CLOEXEC); + + if (cfd >= 0) { + gc570d_handle_ctl_client(cfd); + close(cfd); + } } } @@ -3239,6 +3292,7 @@ int main(int argc, char **argv) if (g_reinit_requested && !atomic_load(&g_daemon_quit)) { fprintf(stderr, "[gc570d INFO] Re-initializing GC570D hardware pipeline in place...\n"); gc570d_everlive_set_dev(NULL); + gc570d_jsonrpc_set_dev(NULL); drv->remove(&g_pdev); probed = false; g_gc = NULL; @@ -3249,6 +3303,7 @@ int main(int argc, char **argv) g_gc = pci_get_drvdata(&g_pdev); probed = true; gc570d_everlive_set_dev(g_gc); + gc570d_jsonrpc_set_dev(g_gc); gc570d_start_user_video_streams(); gc570d_start_user_audio_streams(); clock_gettime(CLOCK_MONOTONIC, &ts_mono_prev); @@ -3266,6 +3321,7 @@ int main(int argc, char **argv) fprintf(stderr, "[gc570d INFO] Shutting down userspace driver...\n"); gc570d_everlive_stop(); + gc570d_jsonrpc_shutdown(); gc570d_ctl_server_shutdown(); #ifdef HAVE_LIBPIPEWIRE gc570d_pipewire_shutdown(); diff --git a/tools/opengc570d-userd/src/gc570d.h b/tools/opengc570d-userd/src/gc570d.h index d81c3fa..2ae3081 100644 --- a/tools/opengc570d-userd/src/gc570d.h +++ b/tools/opengc570d-userd/src/gc570d.h @@ -561,6 +561,8 @@ int gc570d_led_register(struct gc570d_dev *gc); void gc570d_led_unregister(struct gc570d_dev *gc); void gc570d_led_suspend(struct gc570d_dev *gc); int gc570d_led_resume(struct gc570d_dev *gc); +int gc570d_led_set_rgb(struct gc570d_dev *gc, u8 red, u8 green, u8 blue); +int gc570d_led_set_effect(struct gc570d_dev *gc, const char *effect); void gc570d_reset_resume_state(struct gc570d_dev *gc); int gc570d_receiver_read8(struct gc570d_dev *gc, const struct gc570d_i2c_bus *bus, @@ -582,6 +584,12 @@ int gc570d_receiver_write8(struct gc570d_dev *gc, * capture path is running. */ bool gc570d_subsystem_busy(struct gc570d_dev *gc); +/* + * Userspace shim: invoke a registered debugfs-style action node by name + * (for example "it68051_hpd" or "splitter_windows_bridge_edid"). + * Returns the underlying write() result (>= 0), or -ENOENT/-EINVAL. + */ +int gc570d_user_diag_action(const char *name, const char *value); void gc570d_reset_audio_channel(struct gc570d_dev *gc, unsigned int channel); void gc570d_reset_channel(struct gc570d_dev *gc, unsigned int channel); diff --git a/tools/opengc570d-userd/src/gc570d_everlive.c b/tools/opengc570d-userd/src/gc570d_everlive.c index 9d4712d..6873d9d 100644 --- a/tools/opengc570d-userd/src/gc570d_everlive.c +++ b/tools/opengc570d-userd/src/gc570d_everlive.c @@ -1410,3 +1410,104 @@ void gc570d_everlive_push_audio(int port, const uint8_t *pcm, size_t bytes) (void)bytes; #endif } + +/* ------------------------------------------------------------------ */ +/* Read-only accessors for the JSON-RPC control plane */ +/* ------------------------------------------------------------------ */ + +static const char *everlive_osd_pos_name(int pos) +{ + switch (pos) { + case EVERLIVE_OSD_BR: + return "br"; + case EVERLIVE_OSD_BL: + return "bl"; + case EVERLIVE_OSD_TL: + return "tl"; + case EVERLIVE_OSD_TR: + default: + return "tr"; + } +} + +static const char *everlive_aspect_name(int aspect) +{ + switch (aspect) { + case EVERLIVE_ASPECT_16_9: + return "16:9"; + case EVERLIVE_ASPECT_4_3: + return "4:3"; + case EVERLIVE_ASPECT_STRETCH: + return "stretch"; + case EVERLIVE_ASPECT_AUTO: + default: + return "auto"; + } +} + +static const char *everlive_scale_name(int scale) +{ + return scale == EVERLIVE_SCALE_INTEGER ? "integer" : "fit"; +} + +bool gc570d_everlive_running(void) +{ + return atomic_load_explicit(&g_everlive.running, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_headless(void) +{ + if (!atomic_load_explicit(&g_everlive.started, __ATOMIC_RELAXED)) + return true; + return READ_ONCE(g_everlive.opt.headless); +} + +int gc570d_everlive_port(void) +{ + return atomic_load_explicit(&g_everlive.port, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_pip(void) +{ + return atomic_load_explicit(&g_everlive.pip, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_osd(void) +{ + return atomic_load_explicit(&g_everlive.osd, __ATOMIC_RELAXED); +} + +const char *gc570d_everlive_osd_pos(void) +{ + return everlive_osd_pos_name( + atomic_load_explicit(&g_everlive.osd_pos, __ATOMIC_RELAXED)); +} + +const char *gc570d_everlive_aspect(void) +{ + return everlive_aspect_name( + atomic_load_explicit(&g_everlive.aspect, __ATOMIC_RELAXED)); +} + +const char *gc570d_everlive_scale(void) +{ + return everlive_scale_name( + atomic_load_explicit(&g_everlive.scale, __ATOMIC_RELAXED)); +} + +int gc570d_everlive_volume(void) +{ + return atomic_load_explicit(&g_everlive.volume_pct, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_muted(void) +{ + return atomic_load_explicit(&g_everlive.muted, __ATOMIC_RELAXED); +} + +const char *gc570d_everlive_v4l2(int port) +{ + if (port < 0 || port > 1) + return ""; + return g_everlive.opt.v4l2[port] ? g_everlive.opt.v4l2[port] : ""; +} diff --git a/tools/opengc570d-userd/src/gc570d_everlive.h b/tools/opengc570d-userd/src/gc570d_everlive.h index df94e6a..e5e4401 100644 --- a/tools/opengc570d-userd/src/gc570d_everlive.h +++ b/tools/opengc570d-userd/src/gc570d_everlive.h @@ -52,4 +52,17 @@ void gc570d_everlive_set_volume(int volume_pct); void gc570d_everlive_set_mute(bool muted); void gc570d_everlive_push_audio(int port, const uint8_t *pcm, size_t bytes); +/* Read-only accessors for the control plane (safe before start). */ +bool gc570d_everlive_running(void); +bool gc570d_everlive_headless(void); +int gc570d_everlive_port(void); +bool gc570d_everlive_pip(void); +bool gc570d_everlive_osd(void); +const char *gc570d_everlive_osd_pos(void); +const char *gc570d_everlive_aspect(void); +const char *gc570d_everlive_scale(void); +int gc570d_everlive_volume(void); +bool gc570d_everlive_muted(void); +const char *gc570d_everlive_v4l2(int port); + #endif /* GC570D_EVERLIVE_H */ diff --git a/tools/opengc573-userd/.gitignore b/tools/opengc573-userd/.gitignore index 259caa0..351c471 100644 --- a/tools/opengc573-userd/.gitignore +++ b/tools/opengc573-userd/.gitignore @@ -1,6 +1,6 @@ -*.o +src/*.o +src/*.d gc573_user_probe gc573_user_streamer gc573_user_diag everlive_userd -gc573_framegen_selftest diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 735e4a6..e319b77 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -1,58 +1,113 @@ # SPDX-License-Identifier: GPL-2.0-only -# Makefile for OpenGC573 Userspace Driver Tools & EverLive User Daemon +# +# EverLive GC573 (Live Gamer 4K) userspace VFIO engine. +# +# Sources live in src/. Binaries stay in this directory: play.sh, install.sh, +# uninstall.sh and the systemd units resolve them by path. CC ?= gcc -CFLAGS ?= -O2 -Wall -Wextra -pthread -D_GNU_SOURCE +PKG_CONFIG ?= pkg-config +DEPFLAGS := -MMD -MP + +# Flags every translation unit needs. +BASE_CFLAGS := -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare \ + -std=gnu11 -D_GNU_SOURCE +BASE_CFLAGS += -Isrc -I../libeverlive-core LDFLAGS ?= -pthread -SDL_CFLAGS = $(shell pkg-config --cflags sdl2) -SDL_LIBS = $(shell pkg-config --libs sdl2) -# libavcodec/libswresample are only needed by the streamer's bitstream decoder; -# libswscale rescales the V4L2 feed when a reader (OBS) holds a fixed format. -AV_CFLAGS = $(shell pkg-config --cflags libavcodec libavutil libswresample libswscale) -AV_LIBS = $(shell pkg-config --libs libavcodec libavutil libswresample libswscale) -# libpipewire backs the optional "EverLive Capture Audio" tap. -PW_CFLAGS = $(shell pkg-config --cflags libpipewire-0.3) -PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) - -# Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, -# which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. - -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o third_party/jsmn.o -STREAM_OBJS = gc573_user_log.o gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o -TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag -HEADERS = gc573-user-compat.h gc573_user_log.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_rx_eq.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h + +SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 2>/dev/null) +SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 2>/dev/null) +AV_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavcodec libavutil libswresample libswscale 2>/dev/null) +AV_LIBS := $(shell $(PKG_CONFIG) --libs libavcodec libavutil libswresample libswscale 2>/dev/null) +PW_CFLAGS := $(shell $(PKG_CONFIG) --cflags libpipewire-0.3 2>/dev/null) +PW_LIBS := $(shell $(PKG_CONFIG) --libs libpipewire-0.3 2>/dev/null) + +CORE_LIB := ../libeverlive-core/libeverlive_core.a + +# Hardware / register layer: no SDL, no FFmpeg, no PipeWire. +HW_OBJS := \ + src/gc573_vfio.o \ + src/gc573_user_i2c.o \ + src/gc573_user_hw.o \ + src/gc573_user_edid.o \ + src/gc573_user_it6805.o \ + src/gc573_user_it6664.o \ + src/gc573_user_rx_eq.o + +# Capture / presentation pipeline. +STREAM_OBJS := \ + src/gc573_user_log.o \ + src/gc573_user_fpga.o \ + src/gc573_user_video.o \ + src/gc573_user_audio.o \ + src/gc573_user_bitstream.o \ + src/gc573_user_nosignal.o \ + src/gc573_user_ipc.o \ + src/gc573_user_audio_tap.o \ + src/gc573_user_hdr.o \ + src/gc573_user_framegen.o + +ENTRY_OBJS := \ + src/gc573_user_probe.o \ + src/gc573_user_streamer.o \ + src/everlive_userd.o \ + src/gc573_user_diag.o + +TARGETS := gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag all: $(TARGETS) -gc573_user_probe: gc573_user_probe.o $(COMMON_OBJS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +gc573_user_probe: src/gc573_user_probe.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) -gc573_user_streamer: gc573_user_streamer.o $(COMMON_OBJS) $(STREAM_OBJS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(SDL_LIBS) $(AV_LIBS) $(PW_LIBS) -lm +gc573_user_streamer: src/gc573_user_streamer.o $(HW_OBJS) $(STREAM_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) $(SDL_LIBS) $(AV_LIBS) $(PW_LIBS) -lm -everlive_userd: everlive_userd.o $(COMMON_OBJS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +everlive_userd: src/everlive_userd.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) -gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +gc573_user_diag: src/gc573_user_diag.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) -# Headless smoke test for the console cadence detector: a synthetic -# "30 FPS duplicated inside a 60 Hz HDMI container" stream must be recognised -# as 30 unique FPS, including a noisy/dithered variant and a phase slip. -# Pure CPU: the framegen module has no SDL/GL/Vulkan dependency. -gc573_framegen_selftest: test_framegen.o gc573_user_framegen.o - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lm +$(CORE_LIB): + $(MAKE) -C ../libeverlive-core -third_party/jsmn.o: third_party/jsmn.c third_party/jsmn.h - $(CC) $(CFLAGS) -c -o $@ $< +# Headless/hardware TUs: base flags only. +$(HW_OBJS) src/gc573_user_probe.o src/gc573_user_diag.o src/everlive_userd.o: \ + src/%.o: src/%.c + $(CC) $(BASE_CFLAGS) $(DEPFLAGS) -c -o $@ $< -selftest: gc573_framegen_selftest - ./gc573_framegen_selftest +# Capture pipeline: FFmpeg libav* and PipeWire headers. +$(STREAM_OBJS): src/%.o: src/%.c + $(CC) $(BASE_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) $(DEPFLAGS) -c -o $@ $< -%.o: %.c $(HEADERS) - $(CC) $(CFLAGS) $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) -c -o $@ $< +# Only the SDL2 renderer needs SDL headers. +src/gc573_user_streamer.o: src/gc573_user_streamer.c + $(CC) $(BASE_CFLAGS) $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) $(DEPFLAGS) -c -o $@ $< + +# The framegen cadence test now lives in tests/; forward the legacy target. +test selftest: + $(MAKE) -C ../../tests test + +framegen-selftest: + $(MAKE) -C ../../tests test_framegen + ../../tests/test_framegen + +-include $(HW_OBJS:.o=.d) $(STREAM_OBJS:.o=.d) $(ENTRY_OBJS:.o=.d) clean: - rm -f *.o $(TARGETS) gc573_framegen_selftest + rm -f src/*.o src/*.d $(TARGETS) + +PREFIX ?= /usr/local +DESTDIR ?= +BINDIR := $(DESTDIR)$(PREFIX)/bin + +install: all + install -d $(BINDIR) + install -m 0755 gc573_user_streamer everlive_userd gc573_user_probe $(BINDIR)/ + command -v setcap >/dev/null 2>&1 && setcap cap_ipc_lock=+ep $(BINDIR)/gc573_user_streamer 2>/dev/null || true + +uninstall: + rm -f $(BINDIR)/gc573_user_streamer $(BINDIR)/everlive_userd $(BINDIR)/gc573_user_probe -.PHONY: all clean selftest +.PHONY: all test selftest clean install uninstall diff --git a/tools/opengc573-userd/README.md b/tools/opengc573-userd/README.md index e3164fa..24e758b 100644 --- a/tools/opengc573-userd/README.md +++ b/tools/opengc573-userd/README.md @@ -27,7 +27,7 @@ As of **EverLive v1.0.0**, this userspace engine is the default, primary driver 9. **Virtual Camera & Headless Mode:** Native unscaled YUYV frames can be written to a `v4l2loopback` node (`--v4l2 /dev/video10`) for OBS, Discord or FFmpeg, either alongside the gaming window or with `--headless` (no SDL window at all). The writer probes the node with `VIDIOC_TRY_FMT` before every negotiation: if a reader (OBS) already holds a fixed format, that geometry is adopted and every frame is rescaled into it with `libswscale`, so the picture appears immediately instead of staying black. The native mode is retried every 2 s and picked up automatically as soon as the reader releases the format. 10. **Procedural No-Signal Screen:** - When no HDMI signal is present (console off, cable pulled, signal loss mid-session), the engine renders a generated "NO HDMI SIGNAL" card — dark gradient, subtle grid, crossed-out monitor icon, bitmap-font text — and pushes it at ~30 fps to both the SDL window and the loopback node, so OBS never sees a frozen or black feed. The picture is generated once per geometry (`gc573_user_nosignal.[ch]`), costs no CPU while cached, and is replaced seamlessly by the live DMA feed as soon as the signal locks. + When no HDMI signal is present (console off, cable pulled, signal loss mid-session), the engine renders a generated "NO HDMI SIGNAL" card — dark gradient, subtle grid, crossed-out monitor icon, bitmap-font text — and pushes it at ~30 fps to both the SDL window and the loopback node, so OBS never sees a frozen or black feed. The picture is generated once per geometry (`src/gc573_user_nosignal.[ch]`), costs no CPU while cached, and is replaced seamlessly by the live DMA feed as soon as the signal locks. --- @@ -55,10 +55,22 @@ As of **EverLive v1.0.0**, this userspace engine is the default, primary driver ## Building +Sources live in `src/`; the four binaries are written to this directory so +`play.sh`, `install.sh`, `uninstall.sh` and the systemd units resolve them by +path. + +The engine links `../libeverlive-core/libeverlive_core.a` for card discovery, +JSON-RPC helpers, and control-socket resolution. Build the core first (or let +the engine Makefile do it automatically): + ```bash +make -C tools/libeverlive-core make -C tools/opengc573-userd -j"$(nproc)" ``` +From the repository root, `make` (or `./build.sh`) builds the core and both +engines in the correct order. + --- ## Command Line diff --git a/tools/opengc573-userd/everlive_userd.c b/tools/opengc573-userd/src/everlive_userd.c similarity index 100% rename from tools/opengc573-userd/everlive_userd.c rename to tools/opengc573-userd/src/everlive_userd.c diff --git a/tools/opengc573-userd/gc573-user-compat.h b/tools/opengc573-userd/src/gc573-user-compat.h similarity index 100% rename from tools/opengc573-userd/gc573-user-compat.h rename to tools/opengc573-userd/src/gc573-user-compat.h diff --git a/tools/opengc573-userd/gc573_user_audio.c b/tools/opengc573-userd/src/gc573_user_audio.c similarity index 100% rename from tools/opengc573-userd/gc573_user_audio.c rename to tools/opengc573-userd/src/gc573_user_audio.c diff --git a/tools/opengc573-userd/gc573_user_audio.h b/tools/opengc573-userd/src/gc573_user_audio.h similarity index 100% rename from tools/opengc573-userd/gc573_user_audio.h rename to tools/opengc573-userd/src/gc573_user_audio.h diff --git a/tools/opengc573-userd/gc573_user_audio_tap.c b/tools/opengc573-userd/src/gc573_user_audio_tap.c similarity index 100% rename from tools/opengc573-userd/gc573_user_audio_tap.c rename to tools/opengc573-userd/src/gc573_user_audio_tap.c diff --git a/tools/opengc573-userd/gc573_user_audio_tap.h b/tools/opengc573-userd/src/gc573_user_audio_tap.h similarity index 100% rename from tools/opengc573-userd/gc573_user_audio_tap.h rename to tools/opengc573-userd/src/gc573_user_audio_tap.h diff --git a/tools/opengc573-userd/gc573_user_bitstream.c b/tools/opengc573-userd/src/gc573_user_bitstream.c similarity index 100% rename from tools/opengc573-userd/gc573_user_bitstream.c rename to tools/opengc573-userd/src/gc573_user_bitstream.c diff --git a/tools/opengc573-userd/gc573_user_bitstream.h b/tools/opengc573-userd/src/gc573_user_bitstream.h similarity index 100% rename from tools/opengc573-userd/gc573_user_bitstream.h rename to tools/opengc573-userd/src/gc573_user_bitstream.h diff --git a/tools/opengc573-userd/gc573_user_diag.c b/tools/opengc573-userd/src/gc573_user_diag.c similarity index 100% rename from tools/opengc573-userd/gc573_user_diag.c rename to tools/opengc573-userd/src/gc573_user_diag.c diff --git a/tools/opengc573-userd/gc573_user_edid.c b/tools/opengc573-userd/src/gc573_user_edid.c similarity index 100% rename from tools/opengc573-userd/gc573_user_edid.c rename to tools/opengc573-userd/src/gc573_user_edid.c diff --git a/tools/opengc573-userd/gc573_user_edid.h b/tools/opengc573-userd/src/gc573_user_edid.h similarity index 100% rename from tools/opengc573-userd/gc573_user_edid.h rename to tools/opengc573-userd/src/gc573_user_edid.h diff --git a/tools/opengc573-userd/gc573_user_font8x8.h b/tools/opengc573-userd/src/gc573_user_font8x8.h similarity index 100% rename from tools/opengc573-userd/gc573_user_font8x8.h rename to tools/opengc573-userd/src/gc573_user_font8x8.h diff --git a/tools/opengc573-userd/gc573_user_fpga.c b/tools/opengc573-userd/src/gc573_user_fpga.c similarity index 100% rename from tools/opengc573-userd/gc573_user_fpga.c rename to tools/opengc573-userd/src/gc573_user_fpga.c diff --git a/tools/opengc573-userd/gc573_user_fpga.h b/tools/opengc573-userd/src/gc573_user_fpga.h similarity index 100% rename from tools/opengc573-userd/gc573_user_fpga.h rename to tools/opengc573-userd/src/gc573_user_fpga.h diff --git a/tools/opengc573-userd/gc573_user_framegen.c b/tools/opengc573-userd/src/gc573_user_framegen.c similarity index 100% rename from tools/opengc573-userd/gc573_user_framegen.c rename to tools/opengc573-userd/src/gc573_user_framegen.c diff --git a/tools/opengc573-userd/gc573_user_framegen.h b/tools/opengc573-userd/src/gc573_user_framegen.h similarity index 100% rename from tools/opengc573-userd/gc573_user_framegen.h rename to tools/opengc573-userd/src/gc573_user_framegen.h diff --git a/tools/opengc573-userd/gc573_user_hdr.c b/tools/opengc573-userd/src/gc573_user_hdr.c similarity index 100% rename from tools/opengc573-userd/gc573_user_hdr.c rename to tools/opengc573-userd/src/gc573_user_hdr.c diff --git a/tools/opengc573-userd/gc573_user_hdr.h b/tools/opengc573-userd/src/gc573_user_hdr.h similarity index 100% rename from tools/opengc573-userd/gc573_user_hdr.h rename to tools/opengc573-userd/src/gc573_user_hdr.h diff --git a/tools/opengc573-userd/gc573_user_hw.c b/tools/opengc573-userd/src/gc573_user_hw.c similarity index 100% rename from tools/opengc573-userd/gc573_user_hw.c rename to tools/opengc573-userd/src/gc573_user_hw.c diff --git a/tools/opengc573-userd/gc573_user_hw.h b/tools/opengc573-userd/src/gc573_user_hw.h similarity index 100% rename from tools/opengc573-userd/gc573_user_hw.h rename to tools/opengc573-userd/src/gc573_user_hw.h diff --git a/tools/opengc573-userd/gc573_user_i2c.c b/tools/opengc573-userd/src/gc573_user_i2c.c similarity index 100% rename from tools/opengc573-userd/gc573_user_i2c.c rename to tools/opengc573-userd/src/gc573_user_i2c.c diff --git a/tools/opengc573-userd/gc573_user_i2c.h b/tools/opengc573-userd/src/gc573_user_i2c.h similarity index 100% rename from tools/opengc573-userd/gc573_user_i2c.h rename to tools/opengc573-userd/src/gc573_user_i2c.h diff --git a/tools/opengc573-userd/gc573_user_ipc.c b/tools/opengc573-userd/src/gc573_user_ipc.c similarity index 74% rename from tools/opengc573-userd/gc573_user_ipc.c rename to tools/opengc573-userd/src/gc573_user_ipc.c index 85ab000..2bdc244 100644 --- a/tools/opengc573-userd/gc573_user_ipc.c +++ b/tools/opengc573-userd/src/gc573_user_ipc.c @@ -25,16 +25,12 @@ #include #include -/* jsmn JSON parser - single header library (MIT licensed) */ -#define JSMN_HEADER -#define JSMN_PARENT_LINKS -#include "third_party/jsmn.h" +/* JSON-RPC scanning, escaping, and id formatting live in libeverlive-core. */ +#include "everlive_json.h" + +/* Control socket name and path resolution live in libeverlive-core. */ +#include "everlive_ipc.h" -/* - * Control socket name shared by the engine and the Console Hub. - */ -#define IPC_DEFAULT_NAME "everlive.sock" -#define IPC_PATH_LIMIT 100U /* stay clear of sun_path[108] */ #define IPC_REPLY_MAX 3072U #define IPC_METHOD_MAX 48U #define IPC_ID_MAX 48U @@ -122,238 +118,6 @@ static uint64_t ipc_now_ms(void) return (uint64_t)ts.tv_sec * 1000U + (uint64_t)(ts.tv_nsec / 1000000L); } -/* JSON parsing with jsmn - limits: 128 tokens, max depth 16, max input 16KB */ -#define IPC_JSON_MAX_TOKENS 128 -#define IPC_JSON_MAX_DEPTH 16 -#define IPC_JSON_MAX_INPUT (16 * 1024) - -typedef enum { - JSON_GET_STRING, - JSON_GET_LONG, - JSON_GET_DOUBLE, - JSON_GET_BOOL, - JSON_GET_RAW -} json_get_type_e; - -/* Find a key in the JSON token tree. Direct children of root are matched first. */ -static int json_token_find_key(const jsmntok_t *tokens, int tok_count, - const char *key, const char *json) -{ - const size_t klen = strlen(key); - - /* Pass 1: check direct keys of the root object (tokens[0]) */ - for (int i = 1; i < tok_count; i++) { - if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) - continue; - size_t len = (size_t)(tokens[i].end - tokens[i].start); - if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { - if (i + 1 < tok_count) - return i + 1; - } - } - - /* Pass 2: check keys of nested objects (e.g. inside "params") */ - for (int i = 1; i < tok_count; i++) { - if (tokens[i].type != JSMN_STRING) - continue; - if (tokens[i].parent <= 0 || tokens[tokens[i].parent].type != JSMN_OBJECT) - continue; - size_t len = (size_t)(tokens[i].end - tokens[i].start); - if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { - if (i + 1 < tok_count) - return i + 1; - } - } - - return -1; -} - -static bool json_token_to_string(const jsmntok_t *tok, const char *json, - char *out, size_t outsz) -{ - if (!tok || tok->type != JSMN_STRING) - return false; - int len = tok->end - tok->start; - if (len < 0 || (size_t)len >= outsz) - return false; - memcpy(out, json + tok->start, len); - out[len] = '\0'; - return true; -} - -static bool json_token_to_long(const jsmntok_t *tok, const char *json, - long *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - char buf[64]; - int len = tok->end - tok->start; - if (len < 0 || len >= (int)sizeof(buf)) - return false; - memcpy(buf, json + tok->start, len); - buf[len] = '\0'; - char *end; - errno = 0; - long val = strtol(buf, &end, 10); - if (errno || end == buf || *end != '\0') - return false; - *out = val; - return true; -} - -static bool json_token_to_double(const jsmntok_t *tok, const char *json, - double *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - char buf[64]; - int len = tok->end - tok->start; - if (len < 0 || len >= (int)sizeof(buf)) - return false; - memcpy(buf, json + tok->start, len); - buf[len] = '\0'; - char *end; - errno = 0; - double val = strtod(buf, &end); - if (errno || end == buf || *end != '\0') - return false; - *out = val; - return true; -} - -static bool json_token_to_bool(const jsmntok_t *tok, const char *json, - bool *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - int len = tok->end - tok->start; - if (len == 4 && strncmp(json + tok->start, "true", 4) == 0) { - *out = true; - return true; - } - if (len == 5 && strncmp(json + tok->start, "false", 5) == 0) { - *out = false; - return true; - } - return false; -} - -static bool json_token_to_raw(const jsmntok_t *tok, const char *json, - char *out, size_t outsz) -{ - if (!tok || !json || !out || outsz == 0) - return false; - int len = tok->end - tok->start; - if (len < 0 || (size_t)len >= outsz) - return false; - memcpy(out, json + tok->start, len); - out[len] = '\0'; - return true; -} - -static bool json_parse_request(const char *input, size_t len, - const char *key, char *out, size_t outsz, - json_get_type_e type, void *num_out) -{ - if (len >= IPC_JSON_MAX_INPUT) - return false; - - jsmn_parser parser; - jsmntok_t tokens[IPC_JSON_MAX_TOKENS]; - jsmn_init(&parser); - int tok_count = jsmn_parse(&parser, input, len, tokens, IPC_JSON_MAX_TOKENS); - if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) - return false; - - /* Search for the key anywhere in the JSON token tree */ - int val_idx = json_token_find_key(tokens, tok_count, key, input); - if (val_idx < 0) - return false; - - const jsmntok_t *val = &tokens[val_idx]; - switch (type) { - case JSON_GET_STRING: - return json_token_to_string(val, input, out, outsz); - case JSON_GET_LONG: - return json_token_to_long(val, input, (long *)num_out); - case JSON_GET_DOUBLE: - return json_token_to_double(val, input, (double *)num_out); - case JSON_GET_BOOL: - return json_token_to_bool(val, input, (bool *)num_out); - case JSON_GET_RAW: - return json_token_to_raw(val, input, out, outsz); - } - return false; -} - -static bool json_get_string(const char *s, const char *key, char *out, size_t outsz) -{ - if (!s || !key || !out || !outsz) - return false; - return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_STRING, NULL); -} - -static bool json_get_long(const char *s, const char *key, long *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_LONG, out); -} - -static bool json_get_double(const char *s, const char *key, double *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_DOUBLE, out); -} - -static bool json_get_bool(const char *s, const char *key, bool *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_BOOL, out); -} - -/* Legacy wrapper for json_find_value (used in ipc_handle_line for "id" and "method") */ -static bool json_find_value(const char *s, const char *key, char *out, size_t outsz) -{ - if (!s || !key || !out || !outsz) - return false; - /* For "method", extract unquoted string */ - if (strcmp(key, "method") == 0) { - return json_get_string(s, key, out, outsz); - } - /* For "id", return raw token (string value, number, or null) */ - return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_RAW, NULL); -} - -static void json_escape_into(char *dst, size_t dstsz, const char *src) -{ - size_t o = 0; - - if (!dst || dstsz == 0) - return; - if (!src) { - dst[0] = '\0'; - return; - } - while (src && *src && o + 2 < dstsz) { - unsigned char ch = (unsigned char)*src++; - - if (ch == '"' || ch == '\\') { - dst[o++] = '\\'; - dst[o++] = (char)ch; - } else if (ch < 0x20) { - if (o + 6 >= dstsz) - break; - o += (size_t)snprintf(dst + o, dstsz - o, "\\u%04x", ch); - } else { - dst[o++] = (char)ch; - } - } - dst[o] = '\0'; -} - /* ------------------------------------------------------------------ */ /* Wire I/O */ /* ------------------------------------------------------------------ */ @@ -373,49 +137,13 @@ static void ipc_send_raw(struct ipc_client *c, const char *data, size_t len) } } -static void json_format_id(char *dst, size_t dstsz, const char *id) -{ - if (!dst || dstsz == 0) - return; - if (!id || !*id || strcmp(id, "null") == 0) { - snprintf(dst, dstsz, "null"); - return; - } - char *end = NULL; - strtol(id, &end, 10); - if (end && *end == '\0') { - snprintf(dst, dstsz, "%s", id); - return; - } - size_t len = strlen(id); - char inner[IPC_ID_MAX]; - const char *to_esc = id; - if (id[0] == '"' && len >= 2 && id[len - 1] == '"') { - size_t in_len = len - 2; - if (in_len >= sizeof(inner)) - in_len = sizeof(inner) - 1; - memcpy(inner, id + 1, in_len); - inner[in_len] = '\0'; - to_esc = inner; - } - if (dstsz >= 3) { - dst[0] = '"'; - json_escape_into(dst + 1, dstsz - 2, to_esc); - size_t elen = strlen(dst); - dst[elen] = '"'; - dst[elen + 1] = '\0'; - } else { - snprintf(dst, dstsz, "null"); - } -} - static void ipc_reply(struct ipc_client *c, const char *id, const char *result_json) { char esc_id[IPC_ID_MAX * 2]; char buf[IPC_REPLY_MAX]; - json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_format_id(esc_id, sizeof(esc_id), id); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}\n", esc_id, result_json); @@ -431,8 +159,8 @@ static void ipc_error(struct ipc_client *c, const char *id, int code, char esc_msg[256]; char buf[512]; - json_format_id(esc_id, sizeof(esc_id), id); - json_escape_into(esc_msg, sizeof(esc_msg), message ? message : "error"); + everlive_json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_escape_into(esc_msg, sizeof(esc_msg), message ? message : "error"); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s," "\"error\":{\"code\":%d,\"message\":\"%s\"}}\n", @@ -527,13 +255,13 @@ static void ipc_send_status(struct ipc_server *s, struct ipc_client *c, ipc_snapshot_read(&st); age_ms = st.timestamp_ms ? ipc_now_ms() - st.timestamp_ms : 0; - json_escape_into(path_esc, sizeof(path_esc), st.v4l2_path); - json_escape_into(label_esc, sizeof(label_esc), st.audio_label); - json_escape_into(pos_esc, sizeof(pos_esc), st.osd_position); - json_escape_into(aspect_esc, sizeof(aspect_esc), st.aspect); - json_escape_into(dll_esc, sizeof(dll_esc), st.framegen_dll_path); - json_escape_into(scale_esc, sizeof(scale_esc), st.scale); - json_escape_into(layout_esc, sizeof(layout_esc), st.audio_layout); + everlive_json_escape_into(path_esc, sizeof(path_esc), st.v4l2_path); + everlive_json_escape_into(label_esc, sizeof(label_esc), st.audio_label); + everlive_json_escape_into(pos_esc, sizeof(pos_esc), st.osd_position); + everlive_json_escape_into(aspect_esc, sizeof(aspect_esc), st.aspect); + everlive_json_escape_into(dll_esc, sizeof(dll_esc), st.framegen_dll_path); + everlive_json_escape_into(scale_esc, sizeof(scale_esc), st.scale); + everlive_json_escape_into(layout_esc, sizeof(layout_esc), st.audio_layout); n = snprintf(result, sizeof(result), "{\"proto\":%d,\"version\":\"%s\",\"pid\":%ld," @@ -611,7 +339,7 @@ static void ipc_send_hello(struct ipc_server *s, struct ipc_client *c, char path_esc[128]; int n; - json_escape_into(path_esc, sizeof(path_esc), s->path); + everlive_json_escape_into(path_esc, sizeof(path_esc), s->path); n = snprintf(result, sizeof(result), "{\"proto\":%d,\"app\":\"everlive\",\"version\":\"%s\"," "\"pid\":%ld,\"socket\":\"%s\"," @@ -707,8 +435,8 @@ static void ipc_handle_led_set(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_LED_SET }; long r, g, b; - if (!json_get_long(line, "r", &r) || !json_get_long(line, "g", &g) || - !json_get_long(line, "b", &b)) { + if (!everlive_json_get_long(line, "r", &r) || !everlive_json_get_long(line, "g", &g) || + !everlive_json_get_long(line, "b", &b)) { ipc_error(c, id, -32602, "invalid params: r, g, b required"); return; } @@ -726,7 +454,7 @@ static void ipc_handle_led_mode(struct ipc_client *c, const char *id, char mode[24]; int code; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -748,7 +476,7 @@ static void ipc_handle_audio_gain(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_AUDIO_GAIN }; double gain; - if (!json_get_double(line, "gain", &gain)) { + if (!everlive_json_get_double(line, "gain", &gain)) { ipc_error(c, id, -32602, "invalid params: gain required"); return; } @@ -768,7 +496,7 @@ static void ipc_handle_audio_mute(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_AUDIO_MUTE }; bool muted; - if (!json_get_bool(line, "muted", &muted)) { + if (!everlive_json_get_bool(line, "muted", &muted)) { ipc_error(c, id, -32602, "invalid params: muted required"); return; } @@ -784,7 +512,7 @@ static void ipc_handle_audio_channels(struct ipc_client *c, const char *id, char mode[16]; const char *canonical; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -805,7 +533,7 @@ static void ipc_handle_osd_enable(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_OSD_ENABLE }; bool enabled; - if (!json_get_bool(line, "enabled", &enabled)) { + if (!everlive_json_get_bool(line, "enabled", &enabled)) { ipc_error(c, id, -32602, "invalid params: enabled required"); return; } @@ -821,7 +549,7 @@ static void ipc_handle_osd_position(struct ipc_client *c, const char *id, char pos[24]; int code; - if (!json_get_string(line, "pos", pos, sizeof(pos))) { + if (!everlive_json_get_string(line, "pos", pos, sizeof(pos))) { ipc_error(c, id, -32602, "invalid params: pos required"); return; } @@ -843,7 +571,7 @@ static void ipc_handle_aspect(struct ipc_client *c, const char *id, char mode[24]; int code; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -865,7 +593,7 @@ static void ipc_handle_scale(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_SCALE }; char mode[16]; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -892,7 +620,7 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, cmd.f = -999.0f; /* -999 means leave current exposure unchanged */ - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -909,10 +637,10 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, return; } - if (json_get_bool(line, "passthrough", &passthrough)) + if (everlive_json_get_bool(line, "passthrough", &passthrough)) cmd.b = passthrough ? 1 : 0; - if (json_get_double(line, "exposure", &exposure)) { + if (everlive_json_get_double(line, "exposure", &exposure)) { if (exposure < -2.0) exposure = -2.0; if (exposure > 2.0) @@ -931,10 +659,10 @@ static void ipc_handle_hdr_passthrough(struct ipc_client *c, const char *id, cmd.f = -999.0f; /* leave exposure unchanged */ - if (!json_get_bool(line, "enabled", &enable) && - !json_get_bool(line, "passthrough", &enable)) { + if (!everlive_json_get_bool(line, "enabled", &enable) && + !everlive_json_get_bool(line, "passthrough", &enable)) { char mode[16]; - if (json_get_string(line, "mode", mode, sizeof(mode))) + if (everlive_json_get_string(line, "mode", mode, sizeof(mode))) enable = (!strcasecmp(mode, "on") || !strcasecmp(mode, "true") || !strcasecmp(mode, "passthrough")); } @@ -958,7 +686,7 @@ static void ipc_handle_framegen(struct ipc_client *c, const char *id, cmd.c = -1; cmd.f = -1.0f; - if (json_get_string(line, "mode", mode, sizeof(mode))) { + if (everlive_json_get_string(line, "mode", mode, sizeof(mode))) { if (!strcasecmp(mode, "off") || !strcasecmp(mode, "none")) cmd.a = GC573_IPC_FG_OFF; else if (!strcasecmp(mode, "60to120") || @@ -975,16 +703,16 @@ static void ipc_handle_framegen(struct ipc_client *c, const char *id, } } - if (json_get_double(line, "flow_scale", &scale)) { + if (everlive_json_get_double(line, "flow_scale", &scale)) { if (scale < 0.5) scale = 0.5; if (scale > 1.0) scale = 1.0; cmd.f = (float)scale; } - if (json_get_bool(line, "perf_mode", &flag)) + if (everlive_json_get_bool(line, "perf_mode", &flag)) cmd.b = flag ? 1 : 0; - if (json_get_bool(line, "low_latency", &flag)) + if (everlive_json_get_bool(line, "low_latency", &flag)) cmd.c = flag ? 1 : 0; if (cmd.a < 0 && cmd.b < 0 && cmd.c < 0 && cmd.f < 0.0f) { @@ -1002,7 +730,7 @@ static void ipc_handle_mode(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_MODE }; char mode[24]; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -1027,14 +755,14 @@ static void ipc_handle_v4l2(struct ipc_client *c, const char *id, char device[64]; bool enabled; - if (!json_get_bool(line, "enabled", &enabled)) { + if (!everlive_json_get_bool(line, "enabled", &enabled)) { ipc_error(c, id, -32602, "invalid params: enabled required"); return; } cmd.a = enabled ? 1 : 0; - if (json_get_string(line, "device", device, sizeof(device))) { + if (everlive_json_get_string(line, "device", device, sizeof(device))) { /* Only character devices; allowlist /dev/video[0-9]+ */ if (strncmp(device, "/dev/video", 10) != 0) { ipc_error(c, id, -32602, "device must match /dev/video[0-9]+"); @@ -1058,8 +786,8 @@ static void ipc_handle_edid_set(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_EDID }; char name[64]; - if (!json_get_string(line, "profile", name, sizeof(name)) && - !json_get_string(line, "name", name, sizeof(name))) { + if (!everlive_json_get_string(line, "profile", name, sizeof(name)) && + !everlive_json_get_string(line, "name", name, sizeof(name))) { ipc_error(c, id, -32602, "invalid params: profile required"); return; } @@ -1079,9 +807,9 @@ static void ipc_handle_hotplug(struct ipc_client *c, const char *id, bool pulse = true; char action[16]; - if (json_get_bool(line, "pulse", &pulse)) + if (everlive_json_get_bool(line, "pulse", &pulse)) cmd.a = pulse ? 1 : 0; - if (json_get_string(line, "action", action, sizeof(action))) + if (everlive_json_get_string(line, "action", action, sizeof(action))) cmd.a = (strcasecmp(action, "off") != 0 && strcasecmp(action, "disable") != 0 && strcasecmp(action, "0") != 0); @@ -1095,8 +823,8 @@ static void ipc_handle_hdr_exposure(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HDR_EV }; double exposure = 0.0; - if (!json_get_double(line, "exposure", &exposure) && - !json_get_double(line, "ev", &exposure)) { + if (!everlive_json_get_double(line, "exposure", &exposure) && + !everlive_json_get_double(line, "ev", &exposure)) { ipc_error(c, id, -32602, "invalid params: exposure required"); return; } @@ -1115,8 +843,8 @@ static void ipc_handle_toast_show(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_TOAST }; char msg[64] = { 0 }; - if (json_get_string(line, "message", msg, sizeof(msg)) || - json_get_string(line, "text", msg, sizeof(msg))) { + if (everlive_json_get_string(line, "message", msg, sizeof(msg)) || + everlive_json_get_string(line, "text", msg, sizeof(msg))) { snprintf(cmd.path, sizeof(cmd.path), "%s", msg); } else { snprintf(cmd.path, sizeof(cmd.path), "Notification"); @@ -1130,10 +858,10 @@ static void ipc_handle_line(struct ipc_server *s, struct ipc_client *c, { char id[IPC_ID_MAX]; char method[IPC_METHOD_MAX]; - bool has_id = json_find_value(line, "id", id, sizeof(id)) && + bool has_id = everlive_json_find_value(line, "id", id, sizeof(id)) && strcmp(id, "null") != 0; - if (!json_get_string(line, "method", method, sizeof(method))) { + if (!everlive_json_get_string(line, "method", method, sizeof(method))) { if (has_id) ipc_error(c, id, -32700, "parse error: missing method"); return; @@ -1348,11 +1076,8 @@ int gc573_ipc_start(const char *version, const char *socket_override) struct ipc_server *s = &g_ipc; struct sockaddr_un addr; struct epoll_event ev; - const char *dir; const char *stage = "resolve path"; - char candidate[128]; int ret; - int n; if (__atomic_load_n(&s->running, __ATOMIC_ACQUIRE)) return 0; @@ -1367,37 +1092,15 @@ int gc573_ipc_start(const char *version, const char *socket_override) snprintf(s->version, sizeof(s->version), "%s", version && *version ? version : "unknown"); - /* 1. Resolve the socket path. */ - if (socket_override && *socket_override) { - if (strlen(socket_override) >= IPC_PATH_LIMIT) { - ret = -ENAMETOOLONG; - goto fail; - } - snprintf(s->path, sizeof(s->path), "%s", socket_override); - } else { - dir = getenv("XDG_RUNTIME_DIR"); - if (!dir || !*dir || access(dir, W_OK) != 0) { - snprintf(candidate, sizeof(candidate), "/run/user/%u", - (unsigned int)getuid()); - if (access(candidate, W_OK) == 0) { - dir = candidate; - } else { - snprintf(candidate, sizeof(candidate), - "/tmp/everlive-%u", (unsigned int)getuid()); - if (mkdir(candidate, 0700) < 0 && errno != EEXIST) { - ret = -errno; - goto fail; - } - dir = candidate; - } - } - n = snprintf(s->path, sizeof(s->path), "%s/%s", dir, - IPC_DEFAULT_NAME); - if (n < 0 || (size_t)n >= IPC_PATH_LIMIT) { - ret = -ENAMETOOLONG; - goto fail; - } - } + /* + * 1. Resolve the socket path. Precedence: explicit --ipc-socket, + * $EVERLIVE_SOCKET, legacy $EVERLIVE_CONTROL_SOCKET, then the user + * runtime directory. prepare() also creates /tmp/everlive-. + */ + ret = everlive_ipc_prepare_socket_path(s->path, sizeof(s->path), + socket_override); + if (ret < 0) + goto fail; /* 2. Take the lock file — this is the authoritative ownership test. */ stage = "lock file"; diff --git a/tools/opengc573-userd/gc573_user_ipc.h b/tools/opengc573-userd/src/gc573_user_ipc.h similarity index 100% rename from tools/opengc573-userd/gc573_user_ipc.h rename to tools/opengc573-userd/src/gc573_user_ipc.h diff --git a/tools/opengc573-userd/gc573_user_it6664.c b/tools/opengc573-userd/src/gc573_user_it6664.c similarity index 100% rename from tools/opengc573-userd/gc573_user_it6664.c rename to tools/opengc573-userd/src/gc573_user_it6664.c diff --git a/tools/opengc573-userd/gc573_user_it6664.h b/tools/opengc573-userd/src/gc573_user_it6664.h similarity index 100% rename from tools/opengc573-userd/gc573_user_it6664.h rename to tools/opengc573-userd/src/gc573_user_it6664.h diff --git a/tools/opengc573-userd/gc573_user_it6805.c b/tools/opengc573-userd/src/gc573_user_it6805.c similarity index 100% rename from tools/opengc573-userd/gc573_user_it6805.c rename to tools/opengc573-userd/src/gc573_user_it6805.c diff --git a/tools/opengc573-userd/gc573_user_it6805.h b/tools/opengc573-userd/src/gc573_user_it6805.h similarity index 100% rename from tools/opengc573-userd/gc573_user_it6805.h rename to tools/opengc573-userd/src/gc573_user_it6805.h diff --git a/tools/opengc573-userd/gc573_user_log.c b/tools/opengc573-userd/src/gc573_user_log.c similarity index 100% rename from tools/opengc573-userd/gc573_user_log.c rename to tools/opengc573-userd/src/gc573_user_log.c diff --git a/tools/opengc573-userd/gc573_user_log.h b/tools/opengc573-userd/src/gc573_user_log.h similarity index 100% rename from tools/opengc573-userd/gc573_user_log.h rename to tools/opengc573-userd/src/gc573_user_log.h diff --git a/tools/opengc573-userd/gc573_user_nosignal.c b/tools/opengc573-userd/src/gc573_user_nosignal.c similarity index 100% rename from tools/opengc573-userd/gc573_user_nosignal.c rename to tools/opengc573-userd/src/gc573_user_nosignal.c diff --git a/tools/opengc573-userd/gc573_user_nosignal.h b/tools/opengc573-userd/src/gc573_user_nosignal.h similarity index 100% rename from tools/opengc573-userd/gc573_user_nosignal.h rename to tools/opengc573-userd/src/gc573_user_nosignal.h diff --git a/tools/opengc573-userd/gc573_user_probe.c b/tools/opengc573-userd/src/gc573_user_probe.c similarity index 100% rename from tools/opengc573-userd/gc573_user_probe.c rename to tools/opengc573-userd/src/gc573_user_probe.c diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/src/gc573_user_rx_eq.c similarity index 100% rename from tools/opengc573-userd/gc573_user_rx_eq.c rename to tools/opengc573-userd/src/gc573_user_rx_eq.c diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/src/gc573_user_rx_eq.h similarity index 100% rename from tools/opengc573-userd/gc573_user_rx_eq.h rename to tools/opengc573-userd/src/gc573_user_rx_eq.h diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/src/gc573_user_streamer.c similarity index 100% rename from tools/opengc573-userd/gc573_user_streamer.c rename to tools/opengc573-userd/src/gc573_user_streamer.c diff --git a/tools/opengc573-userd/gc573_user_video.c b/tools/opengc573-userd/src/gc573_user_video.c similarity index 100% rename from tools/opengc573-userd/gc573_user_video.c rename to tools/opengc573-userd/src/gc573_user_video.c diff --git a/tools/opengc573-userd/gc573_user_video.h b/tools/opengc573-userd/src/gc573_user_video.h similarity index 100% rename from tools/opengc573-userd/gc573_user_video.h rename to tools/opengc573-userd/src/gc573_user_video.h diff --git a/tools/opengc573-userd/gc573_vfio.c b/tools/opengc573-userd/src/gc573_vfio.c similarity index 86% rename from tools/opengc573-userd/gc573_vfio.c rename to tools/opengc573-userd/src/gc573_vfio.c index 21ced0b..a0d418a 100644 --- a/tools/opengc573-userd/gc573_vfio.c +++ b/tools/opengc573-userd/src/gc573_vfio.c @@ -4,76 +4,35 @@ */ #include "gc573_vfio.h" -#include +#include "everlive_card.h" #include +/* + * Card discovery lives in libeverlive-core. Only descriptors matched as + * "gc573" are accepted: 1461:0054 with subsystem 0x5700 is a GC570D and + * must never be claimed by this engine, and an unreadable subsystem file + * is treated as "unknown" rather than guessed. + */ static int find_gc573_pci_device(char *out_addr, size_t max_len, int *out_group) { - DIR *dir = opendir("/sys/bus/pci/devices"); - if (!dir) { - pr_err("Failed to open /sys/bus/pci/devices: %s", strerror(errno)); - return -errno; - } + struct everlive_device cards[8]; + int count; + int i; - struct dirent *ent; - int found = 0; + count = everlive_detect_installed_cards(cards, ARRAY_SIZE(cards), NULL); - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') + for (i = 0; i < count; i++) { + if (strcmp(cards[i].desc->model_slug, "gc573") != 0) continue; - - char path[512]; - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", ent->d_name); - FILE *f = fopen(path, "r"); - if (!f) + if (cards[i].iommu_group < 0) continue; - - char vendor[32] = ""; - if (fgets(vendor, sizeof(vendor), f)) { - fclose(f); - if (strncasecmp(vendor, "0x1461", 6) == 0) { - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/device", ent->d_name); - f = fopen(path, "r"); - if (f) { - char dev_id[32] = ""; - if (fgets(dev_id, sizeof(dev_id), f)) { - fclose(f); - /* 0x0054 = GC573 / GC555, 0x0055 = GC573 PCIe alternate */ - if (strncasecmp(dev_id, "0x0054", 6) == 0 || - strncasecmp(dev_id, "0x0055", 6) == 0) { - snprintf(out_addr, max_len, "%.32s", ent->d_name); - - /* Find IOMMU group */ - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/iommu_group", ent->d_name); - char link_buf[512]; - ssize_t len = readlink(path, link_buf, sizeof(link_buf) - 1); - if (len > 0) { - link_buf[len] = '\0'; - char *base = strrchr(link_buf, '/'); - if (base) { - *out_group = atoi(base + 1); - found = 1; - break; - } - } - } - } else { - fclose(f); - } - } - } - } else { - fclose(f); - } - } - closedir(dir); - - if (!found) { - pr_err("No AVerMedia GC573 device (1461:0054/0055) found on PCIe bus!"); - return -ENODEV; + snprintf(out_addr, max_len, "%s", cards[i].pci_bdf); + *out_group = cards[i].iommu_group; + return 0; } - return 0; + pr_err("No AVerMedia GC573 device (1461:0054/0055) found on PCIe bus!"); + return -ENODEV; } int gc573_vfio_init(struct gc573_vfio_ctx *ctx, const char *pci_addr) diff --git a/tools/opengc573-userd/gc573_vfio.h b/tools/opengc573-userd/src/gc573_vfio.h similarity index 100% rename from tools/opengc573-userd/gc573_vfio.h rename to tools/opengc573-userd/src/gc573_vfio.h diff --git a/uninstall.sh b/uninstall.sh index 4f4c442..acbc872 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -12,6 +12,7 @@ if [ "$EUID" -ne 0 ]; then fi TARGET_USER="${SUDO_USER:-$USER}" +TARGET_UID="$(id -u "${TARGET_USER}" 2>/dev/null || echo 0)" # Resolve the home directory without eval-ing user-controlled text. TARGET_HOME="$(getent passwd "${TARGET_USER}" | cut -d: -f6)" [ -n "${TARGET_HOME}" ] || TARGET_HOME="/home/${TARGET_USER}" @@ -20,12 +21,19 @@ echo "[*] Deinstalliere EverLive..." # 1. Laufende Daemons beenden killall -9 everlive_userd gc573_user_streamer opengc570d-userd gc570d-ctl 2>/dev/null || true -rm -f "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gc570d/control.sock" 2>/dev/null || true +EVERLIVE_RUNTIME="${XDG_RUNTIME_DIR:-/run/user/${TARGET_UID}}" +rm -f "${EVERLIVE_RUNTIME}/gc570d/control.sock" 2>/dev/null || true +rm -f "${EVERLIVE_RUNTIME}/everlive.sock" "${EVERLIVE_RUNTIME}/everlive.sock.lock" 2>/dev/null || true rm -f /dev/shm/gc570d_v4l2_shm 2>/dev/null || true # 2. VFIO & Memlock Regeln entfernen rm -f /etc/udev/rules.d/99-gc573-vfio.rules rm -f /etc/security/limits.d/99-everlive-memlock.conf +# GC573 engine binaries: +if [ -d "${SCRIPT_DIR}/tools/opengc573-userd" ]; then + make -C "${SCRIPT_DIR}/tools/opengc573-userd" uninstall >/dev/null 2>&1 || true +fi +rm -f /usr/local/bin/gc573_user_streamer /usr/local/bin/everlive_userd /usr/local/bin/gc573_user_probe # GC570D (DUO) packaging installed through tools/opengc570d-userd/Makefile: # binaries, udev rule, memlock limits, systemd user unit and sleep hook. if [ -d "${SCRIPT_DIR}/tools/opengc570d-userd" ]; then @@ -40,6 +48,18 @@ rm -f /usr/local/libexec/gc570d-vfio-bind.sh /usr/local/libexec/gc570d-sleep.sh rm -f /usr/local/lib/systemd/user/opengc570d-userd.service rm -f /etc/udev/rules.d/99-gc570d-vfio.rules +# libeverlive-core (everlive-ctl, library, headers, service unit, service script) +if [ -d "${SCRIPT_DIR}/tools/libeverlive-core" ]; then + make -C "${SCRIPT_DIR}/tools/libeverlive-core" uninstall >/dev/null 2>&1 || true +fi +rm -f /usr/local/bin/everlive-ctl /usr/local/bin/everlive-service +rm -f /usr/local/lib/libeverlive_core.so* /usr/local/lib/libeverlive_core.a +rm -f /usr/local/include/everlive_card.h /usr/local/include/everlive_frame.h \ + /usr/local/include/everlive_ctl.h /usr/local/include/everlive_json.h \ + /usr/local/include/everlive_ipc.h +rm -f /usr/local/lib/systemd/user/everlive.service +rm -f /usr/local/bin/vfio_pci_bind.sh + # 3. Legacy Kernel-Treiber & DKMS Reste entfernen (falls vorhanden) rmmod open_gc573 2>/dev/null || true rm -rf "/lib/modules/$(uname -r)/updates/open_gc573" 2>/dev/null || true