diff --git a/.SRCINFO b/.SRCINFO index 263bbf9..3ed6f9b 100644 --- a/.SRCINFO +++ b/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = coolerdash pkgdesc = Plug-in for CoolerControl that extends the LCD functionality with additional features - pkgver = 3.0.5 + pkgver = 2.1.0 pkgrel = 1 url = https://github.com/damachine/coolerdash install = coolerdash.install diff --git a/.github/workflows/gitlab.yml b/.github/workflows/gitlab.yml index b67efe8..1549773 100644 --- a/.github/workflows/gitlab.yml +++ b/.github/workflows/gitlab.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch complete history for proper sync diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index db4b518..3d35f76 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -45,12 +45,12 @@ jobs: # openSUSE family - distro: "opensuse/tumbleweed" name: "openSUSE Tumbleweed" - pre_install: "zypper refresh" + pre_install: "zypper --gpg-auto-import-keys refresh || true" install_deps: "zypper install -y tar sudo systemd cairo-devel libcurl-devel gcc make pkg-config libjansson-devel google-roboto-fonts file" - distro: "opensuse/leap:16.0" name: "openSUSE Leap 16.0" - pre_install: "zypper refresh" + pre_install: "zypper --gpg-auto-import-keys refresh || true" install_deps: "zypper install -y sudo systemd cairo-devel libcurl-devel gcc make pkg-config libjansson-devel google-roboto-fonts file" container: @@ -125,12 +125,12 @@ jobs: exit 1 fi - # Check CC-Plugin Lib JS files + # Check CC-Plugin Lib JS is NOT installed (served by CoolerControl) if [ -f "/tmp/install-test/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" ]; then - echo "✅ Plugin Lib JS files installed" - else - echo "❌ Plugin Lib JS files not found" + echo "❌ Plugin Lib JS should NOT be installed (served by CoolerControl)" exit 1 + else + echo "✅ Plugin Lib JS correctly not installed (served by CoolerControl)" fi # Check desktop files diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0430c21..6704742 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: commit_sha: ${{ steps.commit.outputs.sha }} steps: - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -178,7 +178,7 @@ jobs: git - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + uses: actions/checkout@v4 with: ref: master fetch-depth: 0 @@ -259,7 +259,7 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -308,7 +308,7 @@ jobs: git push origin "$TAG" - name: Create GitHub Release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare-version.outputs.tag }} name: ${{ needs.prepare-version.outputs.release_msg }} @@ -445,7 +445,7 @@ jobs: ls -lh SHA256SUMS* - name: Upload packages to release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare-version.outputs.tag }} files: | @@ -466,7 +466,7 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Checkout main repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/Makefile b/Makefile index 54f2640..b296b8f 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,33 @@ -.PHONY: clean install uninstall debug logs help detect-distro install-deps check-deps +.PHONY: all clean distclean install install-strip installdirs uninstall check debug logs help detect-distro install-deps check-deps +.DELETE_ON_ERROR: VERSION := $(shell cat VERSION) -SUDO ?= sudo +# Auto-detect: skip sudo when running as root (e.g. sudo make install) +ifeq ($(shell id -u),0) + SUDO ?= +else + SUDO ?= sudo +endif REALOS ?= yes -CC = gcc -CFLAGS = -Wall -Wextra -O2 -std=c99 -march=x86-64-v3 -Iinclude $(shell pkg-config --cflags cairo jansson libcurl) -LIBS = $(shell pkg-config --libs cairo jansson libcurl) -lm +# Compiler +CC ?= gcc +MARCH ?= x86-64-v3 + +# External dependencies (pkg-config, cached) +PKG_CFLAGS := $(shell pkg-config --cflags cairo jansson libcurl) +PKG_LIBS := $(shell pkg-config --libs cairo jansson libcurl) + +# User-overridable flags +CFLAGS ?= -Wall -Wextra -O2 -march=$(MARCH) +CPPFLAGS ?= +LDFLAGS ?= + +# Required project flags (always applied) +override CFLAGS += -std=c99 +override CPPFLAGS += -Iinclude $(PKG_CFLAGS) +LDLIBS = $(PKG_LIBS) -lm + TARGET = coolerdash # Directories @@ -25,6 +46,23 @@ MANIFEST = etc/coolercontrol/plugins/coolerdash/manifest.toml MANPAGE = man/coolerdash.1 README = README.md +# GNU standard install directories +prefix ?= /usr +exec_prefix ?= $(prefix) +libexecdir ?= $(exec_prefix)/libexec +sysconfdir ?= /etc +datarootdir ?= $(prefix)/share +datadir ?= $(datarootdir) +mandir ?= $(datarootdir)/man + +# Install commands +INSTALL ?= install +INSTALL_PROGRAM ?= $(INSTALL) +INSTALL_DATA ?= $(INSTALL) -m 644 + +# Plugin directory (derived) +PLUGINDIR = $(sysconfdir)/coolercontrol/plugins/coolerdash + # Colors for terminal output RED = \033[0;31m GREEN = \033[0;32m @@ -35,15 +73,19 @@ CYAN = \033[0;36m WHITE = \033[1;37m RESET = \033[0m +# Default target (GNU convention) +all: $(TARGET) + # Standard Build Target - Standard C99 project structure $(TARGET): $(OBJDIR) $(BINDIR) $(OBJECTS) $(MAIN_SOURCE) @printf "\n$(PURPLE)Manual Installation Check:$(RESET)\n" @printf "If you see errors about 'conflicting files' or manual installation, run 'make uninstall' and remove leftover files in /opt/coolerdash, /etc/coolerdash, /etc/systemd/system/coolerdash.service.\n\n" @printf "$(CYAN)Compiling $(TARGET) (Standard C99 structure)...$(RESET)\n" @printf "$(BLUE)Structure:$(RESET) src/ include/ build/ bin/\n" + @printf "$(BLUE)CPPFLAGS:$(RESET) $(CPPFLAGS)\n" @printf "$(BLUE)CFLAGS:$(RESET) $(CFLAGS)\n" - @printf "$(BLUE)LIBS:$(RESET) $(LIBS)\n" - $(CC) $(CFLAGS) -o $(BINDIR)/$(TARGET) $(MAIN_SOURCE) $(OBJECTS) $(LIBS) + @printf "$(BLUE)LDLIBS:$(RESET) $(LDLIBS)\n" + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $(BINDIR)/$(TARGET) $(MAIN_SOURCE) $(OBJECTS) $(LDLIBS) @printf "$(GREEN)Build successful: $(BINDIR)/$(TARGET)$(RESET)\n" # Create build directory @@ -61,7 +103,7 @@ $(BINDIR): $(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) @mkdir -p $(dir $@) @printf "$(YELLOW)Compiling module: $<$(RESET)\n" - @$(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + @$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@ -include $(OBJECTS:.o=.d) @@ -88,6 +130,9 @@ clean: rm -rf $(OBJDIR) $(BINDIR) @printf "$(GREEN)Cleanup completed$(RESET)\n" +# GNU standard: distclean removes everything clean does (no autoconf here) +distclean: clean + # Detect Linux distro via release files, os-release as fallback detect-distro: @if [ -f /etc/arch-release ]; then \ @@ -187,11 +232,23 @@ check-deps: printf "$(GREEN)All dependencies found$(RESET)\n"; \ fi +# Run tests (GNU standard target) +check: $(OBJDIR) + @printf "$(CYAN)Running tests...$(RESET)\n" + $(CC) $(CPPFLAGS) $(CFLAGS) -I./src -o $(OBJDIR)/test_scaling tests/test_scaling.c -lm + ./$(OBJDIR)/test_scaling + @printf "$(GREEN)All tests passed$(RESET)\n" + # Install binary to /usr/libexec, plugin data to /etc/coolercontrol/plugins/coolerdash/ install: check-deps $(TARGET) @printf "\n" @printf "$(WHITE)=== COOLERDASH INSTALLATION ===$(RESET)\n" @printf "\n" + @if [ -z "$(DESTDIR)" ] && [ "$(REALOS)" = "yes" ] && [ "$$(id -u)" -ne 0 ]; then \ + printf "$(RED)Error: Installation requires root privileges$(RESET)\n"; \ + printf "$(YELLOW)Run: sudo make install$(RESET)\n"; \ + exit 1; \ + fi @if [ "$(REALOS)" = "yes" ]; then \ printf "$(CYAN)Migration: Checking for legacy files and services...$(RESET)\n"; \ LEGACY_FOUND=0; \ @@ -283,41 +340,46 @@ install: check-deps $(TARGET) fi @printf "\n" @printf "$(CYAN)Installing plugin files...$(RESET)\n" - @install -dm755 "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash" - @install -Dm755 $(BINDIR)/$(TARGET) "$(DESTDIR)/usr/libexec/coolerdash/coolerdash" - @install -m644 $(README) "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/README.md" - @install -m644 CHANGELOG.md "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/CHANGELOG.md" - @install -m644 VERSION "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/VERSION" - @install -m666 etc/coolercontrol/plugins/coolerdash/config.json "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/config.json" - @install -dm755 "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui" - @install -m644 etc/coolercontrol/plugins/coolerdash/ui/index.html "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui/index.html" - @install -m644 etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" - @install -m644 images/shutdown.png "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/shutdown.png" - @install -m644 $(MANIFEST) "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/manifest.toml" - @sed -i 's/{{VERSION}}/$(VERSION)/g' "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/manifest.toml" - @sed -i 's/{{VERSION}}/$(VERSION)/g' "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui/index.html" - @printf " $(GREEN)Binary:$(RESET) $(DESTDIR)/usr/libexec/coolerdash/coolerdash\n" - @printf " $(GREEN)Config JSON:$(RESET) $(DESTDIR)/etc/coolercontrol/plugins/coolerdash/config.json\n" - @printf " $(GREEN)Web UI:$(RESET) $(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui/index.html\n" - @printf " $(GREEN)Plugin Lib:$(RESET) $(DESTDIR)/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js\n" - @printf " $(GREEN)Plugin:$(RESET) $(DESTDIR)/etc/coolercontrol/plugins/coolerdash/manifest.toml\n" + @$(INSTALL) -d "$(DESTDIR)$(PLUGINDIR)" + @$(INSTALL_PROGRAM) -D $(BINDIR)/$(TARGET) "$(DESTDIR)$(libexecdir)/coolerdash/coolerdash" + @$(INSTALL_DATA) $(README) "$(DESTDIR)$(PLUGINDIR)/README.md" + @$(INSTALL_DATA) CHANGELOG.md "$(DESTDIR)$(PLUGINDIR)/CHANGELOG.md" + @$(INSTALL_DATA) VERSION "$(DESTDIR)$(PLUGINDIR)/VERSION" + @if [ -f "$(DESTDIR)$(PLUGINDIR)/config.json" ]; then \ + $(INSTALL) -m 600 etc/coolercontrol/plugins/coolerdash/config.json "$(DESTDIR)$(PLUGINDIR)/config.json.new"; \ + chmod 600 "$(DESTDIR)$(PLUGINDIR)/config.json"; \ + printf " $(YELLOW)Config:$(RESET) Existing config.json preserved (permissions updated to 600). New defaults saved as config.json.new\n"; \ + else \ + $(INSTALL) -m 600 etc/coolercontrol/plugins/coolerdash/config.json "$(DESTDIR)$(PLUGINDIR)/config.json"; \ + fi + @$(INSTALL) -d "$(DESTDIR)$(PLUGINDIR)/ui" + @$(INSTALL_DATA) etc/coolercontrol/plugins/coolerdash/ui/index.html "$(DESTDIR)$(PLUGINDIR)/ui/index.html" + @$(INSTALL_DATA) images/shutdown.png "$(DESTDIR)$(PLUGINDIR)/shutdown.png" + @$(INSTALL_DATA) $(MANIFEST) "$(DESTDIR)$(PLUGINDIR)/manifest.toml" + @sed -i 's/{{VERSION}}/$(VERSION)/g' "$(DESTDIR)$(PLUGINDIR)/manifest.toml" + @sed -i 's/{{VERSION}}/$(VERSION)/g' "$(DESTDIR)$(PLUGINDIR)/ui/index.html" + @printf " $(GREEN)Binary:$(RESET) $(DESTDIR)$(libexecdir)/coolerdash/coolerdash\n" + @printf " $(GREEN)Config JSON:$(RESET) $(DESTDIR)$(PLUGINDIR)/config.json (chmod 600)\n" + @printf " $(GREEN)Web UI:$(RESET) $(DESTDIR)$(PLUGINDIR)/ui/index.html\n" + @printf " $(GREEN)Plugin Lib:$(RESET) Served by CoolerControl at /plugins/lib/cc-plugin-lib.js\n" + @printf " $(GREEN)Plugin:$(RESET) $(DESTDIR)$(PLUGINDIR)/manifest.toml\n" @printf " $(GREEN)Image:$(RESET) shutdown.png (coolerdash.png)\n" @printf " $(GREEN)Documentation:$(RESET) README.md, LICENSE, CHANGELOG.md, VERSION\n" @printf "\n" - @printf "$(CYAN)Note: Plugin binary is available at /usr/libexec/coolerdash/coolerdash$(RESET)\\n" + @printf "$(CYAN)Note: Plugin binary is available at $(libexecdir)/coolerdash/coolerdash$(RESET)\\n" @printf "\n" @printf "$(CYAN)Installing documentation...$(RESET)\n" - @install -Dm644 $(MANPAGE) "$(DESTDIR)/usr/share/man/man1/coolerdash.1" - @printf " $(GREEN)Manual:$(RESET) $(DESTDIR)/usr/share/man/man1/coolerdash.1\n" + @$(INSTALL_DATA) -D $(MANPAGE) "$(DESTDIR)$(mandir)/man1/coolerdash.1" + @printf " $(GREEN)Manual:$(RESET) $(DESTDIR)$(mandir)/man1/coolerdash.1\n" @printf "$(CYAN)Installing license...$(RESET)\n" - @install -Dm644 LICENSE "$(DESTDIR)/usr/share/licenses/coolerdash/LICENSE" - @printf " $(GREEN)License:$(RESET) $(DESTDIR)/usr/share/licenses/coolerdash/LICENSE\n" + @$(INSTALL_DATA) -D LICENSE "$(DESTDIR)$(datarootdir)/licenses/coolerdash/LICENSE" + @printf " $(GREEN)License:$(RESET) $(DESTDIR)$(datarootdir)/licenses/coolerdash/LICENSE\n" @printf "$(CYAN)Installing desktop shortcut...$(RESET)\n" - @install -Dm644 etc/applications/coolerdash.desktop "$(DESTDIR)/usr/share/applications/coolerdash.desktop" - @printf " $(GREEN)Shortcut:$(RESET) $(DESTDIR)/usr/share/applications/coolerdash.desktop\n" + @$(INSTALL_DATA) -D etc/applications/coolerdash.desktop "$(DESTDIR)$(datadir)/applications/coolerdash.desktop" + @printf " $(GREEN)Shortcut:$(RESET) $(DESTDIR)$(datadir)/applications/coolerdash.desktop\n" @printf "$(CYAN)Installing icon...$(RESET)\n" - @install -Dm644 etc/icons/coolerdash.svg "$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/coolerdash.svg" - @printf " $(GREEN)Icon:$(RESET) $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/coolerdash.svg\n" + @$(INSTALL_DATA) -D etc/icons/coolerdash.svg "$(DESTDIR)$(datadir)/icons/hicolor/scalable/apps/coolerdash.svg" + @printf " $(GREEN)Icon:$(RESET) $(DESTDIR)$(datadir)/icons/hicolor/scalable/apps/coolerdash.svg\n" @printf "\n" @printf "$(WHITE)INSTALLATION SUCCESSFUL$(RESET)\n" @printf "\n" @@ -332,11 +394,30 @@ install: check-deps $(TARGET) @printf " $(PURPLE)Show manual:$(RESET) man coolerdash\n" @printf "\n" +# Install with stripped binary (GNU standard target) +install-strip: + $(MAKE) install INSTALL_PROGRAM='$(INSTALL_PROGRAM) -s' + +# Create install directories without installing (GNU standard target) +installdirs: + $(INSTALL) -d "$(DESTDIR)$(libexecdir)/coolerdash" + $(INSTALL) -d "$(DESTDIR)$(PLUGINDIR)" + $(INSTALL) -d "$(DESTDIR)$(PLUGINDIR)/ui" + $(INSTALL) -d "$(DESTDIR)$(mandir)/man1" + $(INSTALL) -d "$(DESTDIR)$(datarootdir)/licenses/coolerdash" + $(INSTALL) -d "$(DESTDIR)$(datadir)/applications" + $(INSTALL) -d "$(DESTDIR)$(datadir)/icons/hicolor/scalable/apps" + # Uninstall Target uninstall: @printf "\n" @printf "$(WHITE)=== COOLERDASH UNINSTALLATION ===$(RESET)\n" @printf "\n" + @if [ -z "$(DESTDIR)" ] && [ "$(REALOS)" = "yes" ] && [ "$$(id -u)" -ne 0 ]; then \ + printf "$(RED)Error: Uninstallation requires root privileges$(RESET)\n"; \ + printf "$(YELLOW)Run: sudo make uninstall$(RESET)\n"; \ + exit 1; \ + fi @if [ "$(REALOS)" = "yes" ]; then \ printf "$(CYAN)Stopping and disabling services...$(RESET)\n"; \ $(SUDO) systemctl stop cc-plugin-coolerdash.service >/dev/null 2>&1 || true; \ @@ -397,14 +478,14 @@ uninstall: LEGACY_FOUND=1; \ fi; \ fi - @$(SUDO) rm -rf "$(DESTDIR)/etc/coolercontrol/plugins/coolerdash" - @$(SUDO) rm -rf "$(DESTDIR)/usr/libexec/coolerdash" - @$(SUDO) rm -rf "$(DESTDIR)/usr/share/licenses/coolerdash" - @$(SUDO) rm -f "$(DESTDIR)/usr/share/man/man1/coolerdash.1" - @$(SUDO) rm -f "$(DESTDIR)/usr/share/applications/coolerdash.desktop" + @$(SUDO) rm -rf "$(DESTDIR)$(PLUGINDIR)" + @$(SUDO) rm -rf "$(DESTDIR)$(libexecdir)/coolerdash" + @$(SUDO) rm -rf "$(DESTDIR)$(datarootdir)/licenses/coolerdash" + @$(SUDO) rm -f "$(DESTDIR)$(mandir)/man1/coolerdash.1" + @$(SUDO) rm -f "$(DESTDIR)$(datadir)/applications/coolerdash.desktop" # Legacy cleanup: remove udev rule if installed by older version @$(SUDO) rm -f "$(DESTDIR)/usr/lib/udev/rules.d/99-coolerdash.rules" - @$(SUDO) rm -f "$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/coolerdash.svg" + @$(SUDO) rm -f "$(DESTDIR)$(datadir)/icons/hicolor/scalable/apps/coolerdash.svg" @if [ "$(REALOS)" = "yes" ]; then \ if id -u coolerdash >/dev/null 2>&1; then \ $(SUDO) userdel -rf coolerdash; \ @@ -417,8 +498,9 @@ uninstall: @printf "\n" # Debug Build -debug: CFLAGS += -g -DDEBUG -fsanitize=address -debug: LIBS += -fsanitize=address +debug: CPPFLAGS += -DDEBUG +debug: CFLAGS += -g -fsanitize=address +debug: LDFLAGS += -fsanitize=address debug: $(TARGET) @printf "$(GREEN)Debug build created with AddressSanitizer: $(BINDIR)/$(TARGET)$(RESET)\n" @@ -434,13 +516,17 @@ help: @printf "$(WHITE)========================================$(RESET)\n" @printf "\n" @printf "$(YELLOW)Build Targets:$(RESET)\n" - @printf " $(GREEN)make$(RESET) - Compiles the program\n" - @printf " $(GREEN)make clean$(RESET) - Removes compiled files\n" - @printf " $(GREEN)make debug$(RESET) - Debug build with AddressSanitizer\n" + @printf " $(GREEN)make$(RESET) - Compiles the program\n" + @printf " $(GREEN)make clean$(RESET) - Removes compiled files\n" + @printf " $(GREEN)make distclean$(RESET) - Same as clean (no autoconf)\n" + @printf " $(GREEN)make check$(RESET) - Runs unit tests\n" + @printf " $(GREEN)make debug$(RESET) - Debug build with AddressSanitizer\n" @printf "\n" @printf "$(YELLOW)Installation:$(RESET)\n" - @printf " $(GREEN)make install$(RESET) - Installs binary + plugin data + systemd units\n" - @printf " $(GREEN)make uninstall$(RESET)- Uninstalls the program\n" + @printf " $(GREEN)make install$(RESET) - Installs binary + plugin data\n" + @printf " $(GREEN)make install-strip$(RESET)- Installs with stripped binary\n" + @printf " $(GREEN)make installdirs$(RESET) - Creates install directories only\n" + @printf " $(GREEN)make uninstall$(RESET) - Uninstalls the program\n" @printf "\n" @printf "$(YELLOW)Plugin Management:$(RESET)\n" @printf " $(GREEN)systemctl enable --now coolercontrold.service$(RESET) - Active CoolerControl service\n" @@ -457,7 +543,7 @@ help: @printf " $(GREEN)README.md$(RESET) - English (main documentation)\n" @printf "\n" @printf "$(YELLOW)Version Usage:$(RESET)\n" - @printf " $(GREEN)Program:$(RESET) /usr/libexec/coolerdash/coolerdash [mode]\n" - @printf " $(GREEN)Config:$(RESET) /etc/coolercontrol/plugins/coolerdash/config.json\n" + @printf " $(GREEN)Program:$(RESET) $(libexecdir)/coolerdash/coolerdash [mode]\n" + @printf " $(GREEN)Config:$(RESET) $(PLUGINDIR)/config.json\n" @printf " $(GREEN)Web UI:$(RESET) CoolerControl Plugin Settings\n" @printf "\n" diff --git a/PKGBUILD b/PKGBUILD index 7deb63b..6dbb2ae 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -42,12 +42,11 @@ build() { make # Copy files to srcdir for packaging (fakeroot cannot access startdir) - mkdir -p "${srcdir}/bin" "${srcdir}/images" "${srcdir}/man" "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui" "${srcdir}/etc/applications" "${srcdir}/etc/icons" "${srcdir}/etc/udev/rules.d" + mkdir -p "${srcdir}/bin" "${srcdir}/images" "${srcdir}/man" "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui" "${srcdir}/etc/applications" "${srcdir}/etc/icons" cp -a bin/coolerdash "${srcdir}/bin/coolerdash" cp -a README.md CHANGELOG.md VERSION LICENSE "${srcdir}/" cp -a etc/coolercontrol/plugins/coolerdash/config.json "${srcdir}/etc/coolercontrol/plugins/coolerdash/" cp -a etc/coolercontrol/plugins/coolerdash/ui/index.html "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui/" - cp -a etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui/" cp -a images/shutdown.png "${srcdir}/images/" cp -a man/coolerdash.1 "${srcdir}/man/" cp -a etc/coolercontrol/plugins/coolerdash/manifest.toml "${srcdir}/etc/coolercontrol/plugins/coolerdash/" @@ -75,11 +74,10 @@ package() { install -Dm644 "${srcdir}/README.md" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/README.md" install -Dm644 "${srcdir}/VERSION" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/VERSION" install -Dm644 "${srcdir}/CHANGELOG.md" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/CHANGELOG.md" - install -Dm666 "${srcdir}/etc/coolercontrol/plugins/coolerdash/config.json" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/config.json" + install -Dm600 "${srcdir}/etc/coolercontrol/plugins/coolerdash/config.json" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/config.json" install -dm755 "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui" install -m644 "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui/index.html" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui/index.html" - install -m644 "${srcdir}/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" install -Dm644 "${srcdir}/images/shutdown.png" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/shutdown.png" install -Dm644 "${srcdir}/etc/coolercontrol/plugins/coolerdash/manifest.toml" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/manifest.toml" diff --git a/VERSION b/VERSION index eca690e..fd2a018 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.5 +3.1.0 diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 17f2310..012081f 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -61,11 +61,10 @@ package() { install -m644 "${srcdir}/${pkgname}/README.md" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/README.md" install -m644 "${srcdir}/${pkgname}/VERSION" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/VERSION" install -m644 "${srcdir}/${pkgname}/CHANGELOG.md" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/CHANGELOG.md" - install -m666 "${srcdir}/${pkgname}/etc/coolercontrol/plugins/coolerdash/config.json" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/config.json" + install -m600 "${srcdir}/${pkgname}/etc/coolercontrol/plugins/coolerdash/config.json" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/config.json" install -dm755 "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui" install -m644 "${srcdir}/${pkgname}/etc/coolercontrol/plugins/coolerdash/ui/index.html" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui/index.html" - install -m644 "${srcdir}/${pkgname}/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js" install -m644 "${srcdir}/${pkgname}/images/shutdown.png" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/shutdown.png" install -m644 "${srcdir}/${pkgname}/etc/coolercontrol/plugins/coolerdash/manifest.toml" "${pkgdir}/etc/coolercontrol/plugins/coolerdash/manifest.toml" diff --git a/aur/coolerdash.install b/aur/coolerdash.install index 492dd11..7c9ce85 100644 --- a/aur/coolerdash.install +++ b/aur/coolerdash.install @@ -41,6 +41,9 @@ post_install() { rm -rf /etc/systemd/system/cc-plugin-coolerdash.service.d rm -f /usr/lib/udev/rules.d/99-coolerdash.rules + # Ensure correct permissions on config.json + chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true + systemctl daemon-reload # Restart plugin service directly if it already exists (reinstall case) @@ -66,6 +69,9 @@ post_upgrade() { rm -rf /etc/systemd/system/cc-plugin-coolerdash.service.d rm -f /usr/lib/udev/rules.d/99-coolerdash.rules + # Ensure correct permissions on config.json + chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true + systemctl daemon-reload # Restart plugin @@ -75,6 +81,8 @@ post_upgrade() { echo "================================================================" echo "CoolerDash upgraded successfully." + echo "Note: config.json is preserved. If a new template was shipped," + echo " compare it with /etc/coolercontrol/plugins/coolerdash/config.json.pacnew" echo "================================================================" } diff --git a/coolerdash.install b/coolerdash.install index 492dd11..7c9ce85 100644 --- a/coolerdash.install +++ b/coolerdash.install @@ -41,6 +41,9 @@ post_install() { rm -rf /etc/systemd/system/cc-plugin-coolerdash.service.d rm -f /usr/lib/udev/rules.d/99-coolerdash.rules + # Ensure correct permissions on config.json + chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true + systemctl daemon-reload # Restart plugin service directly if it already exists (reinstall case) @@ -66,6 +69,9 @@ post_upgrade() { rm -rf /etc/systemd/system/cc-plugin-coolerdash.service.d rm -f /usr/lib/udev/rules.d/99-coolerdash.rules + # Ensure correct permissions on config.json + chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true + systemctl daemon-reload # Restart plugin @@ -75,6 +81,8 @@ post_upgrade() { echo "================================================================" echo "CoolerDash upgraded successfully." + echo "Note: config.json is preserved. If a new template was shipped," + echo " compare it with /etc/coolercontrol/plugins/coolerdash/config.json.pacnew" echo "================================================================" } diff --git a/docs/config-guide.md b/docs/config-guide.md index 7969f39..aa09171 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -1,25 +1,15 @@ # CoolerDash Configuration Guide -Complete guide for configuring CoolerDash through the `config.json` file. +Config file: `/etc/coolercontrol/plugins/coolerdash/config.json` -## Configuration File Location - -``` -/etc/coolercontrol/plugins/coolerdash/config.json -``` - -## Applying Changes - -Restaring CoolerControl reloads the plugin and picks up config changes: +Restart to apply changes: ```bash sudo systemctl restart coolercontrold ``` --- -## Daemon Settings - -Connection to the CoolerControl daemon. +## Daemon ```json "daemon": { @@ -30,18 +20,13 @@ Connection to the CoolerControl daemon. | Key | Default | Description | |-----|---------|-------------| -| `address` | `http://localhost:11987` | API endpoint | -| `access_token` | `""` | Bearer token from CoolerControl UI → Access Protection. Format: `cc_`. Required for authenticated API access. | - -Generate a token in CoolerControl UI under **Access Protection** and paste it into `access_token`. +| `address` | `http://localhost:11987` | CoolerControl API endpoint | +| `access_token` | `""` | Bearer token (`cc_`) from CoolerControl UI > Access Protection | --- -## File Paths +## Paths -System file and directory locations. - -### Example ```json "paths": { "images": "/etc/coolercontrol/plugins/coolerdash", @@ -50,140 +35,52 @@ System file and directory locations. } ``` -### Settings -- **`images`**: Directory for generated display images -- **`image_coolerdash`**: Generated display image path -- **`image_shutdown`**: Image displayed on daemon shutdown +| Key | Description | +|-----|-------------| +| `images` | Directory for generated images | +| `image_coolerdash` | Generated display image path | +| `image_shutdown` | Image shown on daemon shutdown | --- -## 🖥️ Display Settings - -LCD display configuration tested with NZXT Kraken 2023. +## Display -### Basic Configuration ```json "display": { "mode": "dual", "width": 0, "height": 0, - "refresh_interval": 2.5, + "refresh_interval": 3.5, "brightness": 80, "orientation": 0, "shape": "auto", - "circle_switch_interval": 5, + "circle_switch_interval": 8, "content_scale_factor": 0.98, - "inscribe_factor": 0.70710678 + "inscribe_factor": 0.70710678, + "sensor_slot_1": "cpu", + "sensor_slot_2": "liquid", + "sensor_slot_3": "gpu" } ``` -> `width`/`height` set to `0` means CoolerDash queries the actual device dimensions from the API at startup. - -### Settings -- **`mode`**: Display mode: `dual` (default) or `circle` -- **`width`** / **`height`**: Screen dimensions in pixels; `0` = auto-detect from API -- **`refresh_interval`**: Update interval in seconds (0.01–60.0, default: `2.5`) -- **`brightness`**: LCD brightness 0–100% (default: `80`) -- **`orientation`**: Screen rotation: `0`, `90`, `180`, `270` degrees -- **`shape`**: Display shape: `auto` (default), `rectangular`, or `circular` -- **`circle_switch_interval`**: Slot switch interval for circle mode in seconds (1–60, default: `5`) -- **`content_scale_factor`**: Safe area percentage (0.5–1.0, default: `0.98`) -- **`inscribe_factor`**: Inscribe factor for circular displays (default: `0.70710678` = 1/√2) -- **`sensor_slot_1`**: Sensor for slot 1: `cpu`, `gpu`, or `liquid` (default: `cpu`) -- **`sensor_slot_2`**: Sensor for slot 2 (default: `liquid`) -- **`sensor_slot_3`**: Sensor for slot 3 (default: `gpu`) - -Sensor slots control which sensor appears in each display position for both dual and circle mode. - -### Brightness Examples -```json -"brightness": 40, // dim for night use -"brightness": 80, // recommended default -"brightness": 100 // maximum -``` - -### Refresh Rate Examples -```json -"refresh_interval": 1.0, // fast -"refresh_interval": 2.5, // default -"refresh_interval": 5.0 // power-saving -``` - -### Display Shape Override - -The `shape` parameter allows manual control of the **inscribe factor** used for layout calculations: - -- **`auto`** (default): Automatic detection based on device database -- **`rectangular`**: Force inscribe factor = 1.0 (use full display width) -- **`circular`**: Force inscribe factor = 0.7071 (inscribed square for round displays) - -**When to use:** -- Testing different layouts on your display -- Troubleshooting clipping issues on circular displays -- Overriding auto-detection if it's incorrect for your device - -**Examples:** -```json -"shape": "auto" // recommended -"shape": "rectangular" // full width, inscribe_factor = 1.0 -"shape": "circular" // inscribed square, inscribe_factor = 0.7071 -``` - -**Priority:** `shape` config > auto-detection - -**Note:** See [Display Detection Guide](display-detection.md) for technical details about inscribe factors. - -#### Circle Mode Sensor Switching - -**Parameter:** `circle_switch_interval` - -Controls how frequently the circle mode rotates between available sensors: - -- **Range:** 1-60 seconds -- **Default:** 5 seconds -- **Applies to:** Circle mode only - -**Example:** -```json -"mode": "circle", -"circle_switch_interval": 10 -``` - -**Use Cases:** -- **Fast switching (1-3s):** Quick overview of all sensors -- **Moderate (5-8s):** Default balanced viewing -- **Slow switching (10-60s):** Focus on individual sensors longer - -#### Content Scale Factor - -**Parameter:** `content_scale_factor` - -Controls the safe area percentage used for rendering content (determines margin/padding): - -- **Range:** 0.5-1.0 (50%-100%) -- **Default:** 0.98 (98% = 2% margin) -- **Applies to:** Both dual and circle modes - -**Example:** -```json -"content_scale_factor": 0.95 -``` - -**Use Cases:** -- **0.98-1.0:** Minimal margins, maximum screen usage -- **0.90-0.97:** Comfortable padding, safe for text rendering -- **0.70-0.89:** Extra margins, conservative layout -- **0.5-0.69:** Large margins, centered content focus - -**Visual Impact:** -- Higher values (0.95-1.0) = content fills more screen area, less padding -- Lower values (0.5-0.8) = more white space around content, safer margins +| Key | Default | Description | +|-----|---------|-------------| +| `mode` | `dual` | `dual` or `circle` | +| `width` / `height` | `0` | Pixels. `0` = auto-detect from API | +| `refresh_interval` | `3.5` | Update interval in seconds (0.01–60.0) | +| `brightness` | `80` | LCD brightness 0–100% | +| `orientation` | `0` | Rotation: `0`, `90`, `180`, `270` | +| `shape` | `auto` | `auto`, `rectangular`, `circular` | +| `circle_switch_interval` | `8` | Sensor rotation interval in circle mode (1–60s) | +| `content_scale_factor` | `0.98` | Safe area percentage (0.5–1.0) | +| `inscribe_factor` | `0.70710678` | Inscribe factor for circular displays (1/√2) | +| `sensor_slot_1/2/3` | `cpu`/`liquid`/`gpu` | Sensor assignment per slot | + +`shape` overrides auto-detection. Priority: `shape` config > auto-detection. --- -## Visual Layout - -All values are in pixels unless noted. Positions are calculated dynamically from display dimensions. +## Layout ```json "layout": { @@ -202,20 +99,20 @@ All values are in pixels unless noted. Positions are calculated dynamically from | Key | Default | Description | |-----|---------|-------------| -| `bar_height` | `24` | Bar height in pixels | -| `bar_width` | `98` | Bar width in % of display width | -| `bar_gap` | `12` | Gap between bars in pixels | -| `bar_border` | `1.0` | Border thickness in pixels | -| `bar_border_enabled` | `1` | Enable bar border (`1`/`0`) | +| `bar_height` | `24` | Bar height (px) | +| `bar_width` | `98` | Bar width (% of display width) | +| `bar_gap` | `12` | Gap between bars (px) | +| `bar_border` | `1.0` | Border thickness (px) | +| `bar_border_enabled` | `1` | Border on/off (`1`/`0`) | | `label_margin_left` | `1` | Left label margin multiplier | -| `label_margin_bar` | `1` | Margin between label and bar | -| `bar_height_1/2/3` | `0` | Per-slot bar height override. `0` = use `bar_height` | +| `label_margin_bar` | `1` | Label-to-bar margin multiplier | +| `bar_height_1/2/3` | `0` | Per-slot height override. `0` = use `bar_height` | --- ## Colors -RGB color values (0–255). +RGB values (0–255): ```json "colors": { @@ -228,26 +125,28 @@ RGB color values (0–255). --- -## Font Settings +## Font ```json "font": { "face": "Roboto Black", "size_temp": 0, - "size_labels": 0 + "size_labels": 0, + "font_growth_factor": 1.33 } ``` -- **`face`**: Font family name (must be installed, default: `Roboto Black`) -- **`size_temp`** / **`size_labels`**: Font size in points. `0` = auto-scale based on display resolution - - 240×240: ~100pt temp / ~30pt labels - - Formula: `100.0 × (width + height) / (2 × 240.0)` +| Key | Default | Description | +|-----|---------|-------------| +| `face` | `Roboto Black` | Font family (must be installed) | +| `size_temp` / `size_labels` | `0` | Font size (pt). `0` = auto-scale from resolution | +| `font_growth_factor` | `1.33` | Scaling multiplier for auto-sized fonts | --- ## Temperature Zones -Four color zones based on temperature thresholds. Configured per-sensor in the `sensors` section. +Four color zones per sensor. Bars transition through colors as temperature rises. ```json "sensors": { @@ -259,39 +158,36 @@ Four color zones based on temperature thresholds. Configured per-sensor in the ` "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, - "threshold_4_color": { "r": 255, "g": 0, "b": 0 } - }, - "gpu": { ... } + "threshold_4_color": { "r": 255, "g": 0, "b": 0 }, + "offset_x": 0, + "offset_y": 0 + } } ``` -`max_scale` sets the temperature at 100% bar fill. Bars transition through 4 color zones as temperature rises through the thresholds. +`max_scale` = temperature at 100% bar fill. Same structure for `gpu` and `liquid`. --- -## Complete Example +## Positioning -See the default config at `/etc/coolercontrol/plugins/coolerdash/config.json` for a full reference. The file is installed with all options and their defaults. +```json +"positioning": { + "degree_spacing": 16, + "label_offset_x": 0, + "label_offset_y": 0, + "margin_top": 0, + "margin_bottom": 0 +} +``` --- ## Troubleshooting -1. **Display not updating**: Check `refresh_interval` and restart CoolerControl -2. **Wrong colors**: Verify RGB values are 0–255 -3. **Text clipped on circular display**: Adjust `inscribe_factor` and `content_scale_factor` -4. **Bars too wide**: Lower `bar_width` value - -### Backup +- **Display not updating**: Check `refresh_interval`, restart CoolerControl +- **Wrong colors**: RGB values must be 0–255 +- **Text clipped**: Adjust `inscribe_factor` and `content_scale_factor` +- **Bars too wide**: Lower `bar_width` -```bash -sudo cp /etc/coolercontrol/plugins/coolerdash/config.json \ - /etc/coolercontrol/plugins/coolerdash/config.json.backup -``` - -## Developer: Scaling Unit Test - -```bash -gcc -std=c99 -Iinclude -I./src -o build/test_scaling tests/test_scaling.c -lm -./build/test_scaling -``` +Full default config: `/etc/coolercontrol/plugins/coolerdash/config.json` diff --git a/docs/coolercontrol-api.md b/docs/coolercontrol-api.md index 25b4cdb..3072cac 100644 --- a/docs/coolercontrol-api.md +++ b/docs/coolercontrol-api.md @@ -1,152 +1,49 @@ -# CoolerControl API Integration - Developer Guide +# CoolerControl API Integration -## Table of Contents -- [Overview](#overview) -- [Architecture](#architecture) -- [Module cc_main](#module-cc_main) -- [Module cc_conf](#module-cc_conf) -- [Module cc_sensor](#module-cc_sensor) -- [API Communication Flow](#api-communication-flow) -- [Data Structures](#data-structures) -- [Error Handling](#error-handling) -- [Best Practices](#best-practices) -- [Troubleshooting](#troubleshooting) +## Modules ---- - -## Overview - -The CoolerControl API integration consists of three core modules that handle all communication with the CoolerControl daemon's REST API: - -- **cc_main.c/h**: Session management, authentication, and LCD image upload -- **cc_conf.c/h**: Device caching, display detection, and configuration utilities -- **cc_sensor.c/h**: Temperature data retrieval from CPU and GPU sensors - -### Purpose - -CoolerDash acts as a specialized LCD client for CoolerControl, fetching temperature data and uploading rendered images to CoolerControl-managed devices with LCD displays. - -### API Endpoints Used +| Module | File | Purpose | +|--------|------|---------| +| cc_main | `src/srv/cc_main.c/h` | Session management, auth, LCD upload | +| cc_conf | `src/srv/cc_conf.c/h` | Device cache, display detection | +| cc_sensor | `src/srv/cc_sensor.c/h` | Temperature retrieval | -``` -GET /devices - Device enumeration -POST /status - Temperature sensor data -PUT /devices/{uid}/settings/lcd/lcd/images - LCD image upload -PUT /devices/{uid}/settings/lcd/lcd/shutdown-image - Shutdown image registration (CC4 only) -``` +## API Endpoints -### Dependencies +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/devices` | GET | Device enumeration (once at startup) | +| `/status` | POST | Temperature sensor data | +| `/devices/{uid}/settings/lcd/lcd/images` | PUT | LCD image upload | +| `/devices/{uid}/settings/lcd/lcd/shutdown-image` | PUT | Shutdown image registration (CC4) | -- **libcurl**: HTTP client for REST API communication -- **jansson**: JSON parsing and generation -- **CoolerControl Daemon**: Must be running and accessible (default: `http://127.0.0.1:11987`) +Base URL: `http://localhost:11987` (configurable) +Auth: `Authorization: Bearer cc_` --- -## Architecture +## Session Lifecycle -### Module Relationships - -``` -┌─────────────────────────────────────────────────────────┐ -│ main.c │ -│ (Main Event Loop) │ -└──────────────────┬──────────────────────────────────────┘ - │ - │ Initialization & Data Retrieval - │ - ┌───────────┴───────────┬──────────────────────────┐ - │ │ │ -┌──────▼──────┐ ┌───────▼────────┐ ┌────────▼────────┐ -│ cc_main.c │ │ cc_conf.c │ │ cc_sensor.c │ -│ Session │◄──────┤ Device │ │ Temperature │ -│ Auth │ │ Cache │ │ Data │ -│ Upload │ │ Detection │ │ Retrieval │ -└─────────────┘ └────────────────┘ └─────────────────┘ - │ │ │ - │ │ │ - └───────────────────────┴──────────────────────────┘ - │ - │ libcurl + jansson - │ - ┌──────────▼───────────┐ - │ CoolerControl │ - │ Daemon (REST API) │ - └──────────────────────┘ ``` +1. init_coolercontrol_session() + - curl_global_init() + curl_easy_init() + - Build "Authorization: Bearer cc_" header + - session_initialized = 1 -### Initialization Sequence - -```c -// 1. Configuration loading (config.c) -Config config = {0}; -load_plugin_config(&config, CONFIG_PATH); - -// 2. CoolerControl session initialization -init_coolercontrol_session(&config); +2. Main loop (reuse session) + - get_temperature_data() → POST /status + - send_image_to_lcd() → PUT /devices/{uid}/.../images -// 3. Device cache initialization -init_device_cache(&config); - -// 4. Update config with device dimensions (if needed) -update_config_from_device(&config); - -// 5. Main loop: fetch data → render → upload -while (running) { - get_temperature_monitor_data(&config, &data); - draw_display_image(&config); // Dispatches to dual or circle mode - // send_image_to_lcd() called internally - sleep(update_interval); -} - -// 6. Cleanup -cleanup_coolercontrol_session(); +3. cleanup_coolercontrol_session() + - curl_easy_cleanup() + curl_global_cleanup() + - session_initialized = 0 ``` --- -## Module cc_main - -### Purpose -Handles HTTP session management, authentication, and LCD image upload via multipart PUT requests. - -### File: `src/srv/cc_main.c` (600 lines) - -### Key Components - -#### 1. HTTP Response Buffer - -**Purpose**: Dynamic memory management for HTTP responses - -```c -typedef struct http_response { - char *data; - size_t size; - size_t capacity; -} http_response; -``` - -**Functions**: -- `cc_init_response_buffer()`: Allocate with initial capacity (default: 4096-8192 bytes) -- `cc_cleanup_response_buffer()`: Free memory and reset state -- `reallocate_response_buffer()`: Exponential growth (1.5x or exact fit) - -**Growth Strategy**: -```c -new_capacity = max(required_size, capacity * 3/2) -``` - -**Usage Pattern**: -```c -http_response response = {0}; -cc_init_response_buffer(&response, 8192); -// ... use response ... -cc_cleanup_response_buffer(&response); -``` - -#### 2. Session State +## cc_main — Session & Upload -**Purpose**: Maintain persistent CURL session with Bearer-token authentication +### Session State ```c typedef struct { @@ -154,260 +51,55 @@ typedef struct { char access_token[CC_BEARER_HEADER_SIZE]; int session_initialized; } CoolerControlSession; - -static CoolerControlSession cc_session = { - .curl_handle = NULL, - .access_token = {0}, - .session_initialized = 0 -}; ``` -#### 3. Authentication Flow - -**Function**: `init_coolercontrol_session(const Config *config)` +### Public API -CoolerDash uses one authentication mode only: +| Function | Purpose | +|----------|---------| +| `init_coolercontrol_session(config)` | Init CURL + Bearer header | +| `is_session_initialized()` | Check session state | +| `cleanup_coolercontrol_session()` | Free CURL resources | +| `send_image_to_lcd(config, image_path, device_uid)` | Upload PNG via multipart PUT | -- `access_token` must be set in config -- All requests use `Authorization: Bearer cc_` -- Token is generated in CoolerControl UI under **Access Protection** -- No legacy login or session fallback is used +### LCD Upload ---- - -#### 4. Shutdown Image Registration (CC4) - -**Function**: `register_shutdown_image_with_cc(const Config *config, const char *image_path, const char *device_uid)` - -Called **once at startup** after device initialization. Registers `shutdown.png` with CC4: - -- Endpoint: `PUT {address}/devices/{uid}/settings/lcd/lcd/shutdown-image` -- Same multipart form as live image upload -- CC4 stores image server-side; displays it when CoolerControl stops -- Returns gracefully on 404 when the endpoint is unavailable - -**Implementation**: -```c -int init_coolercontrol_session(const Config *config) -{ - if (!config || config->access_token[0] == '\0') - return 0; - - curl_global_init(CURL_GLOBAL_DEFAULT); - cc_session.curl_handle = curl_easy_init(); - - snprintf(cc_session.access_token, sizeof(cc_session.access_token), - "Authorization: Bearer %s", config->access_token); - cc_session.session_initialized = 1; - return 1; -} ``` +PUT {address}/devices/{uid}/settings/lcd/lcd/images?log=false +Content-Type: multipart/form-data -**Security Notes**: -- Bearer token is attached explicitly to every authenticated request -- No fallback authentication path exists - -#### 4. LCD Image Upload - -**Function**: `send_image_to_lcd(const Config *config, const char *image_path, const char *device_uid)` - -**Purpose**: Upload rendered PNG to device LCD via multipart PUT request - -**URL Format**: -``` -PUT {daemon_address}/devices/{device_uid}/settings/lcd/lcd/images?log=false -``` - -**Multipart Form Fields**: -- `mode`: "image" (static image mode) -- `brightness`: Integer 0-100 (from config) -- `orientation`: Integer 0-270 (from config) -- `images[]`: PNG file (Content-Type: image/png) - -**Implementation**: -```c -int send_image_to_lcd(const Config *config, const char *image_path, const char *device_uid) -{ - // 1. Validate parameters - if (!cc_session.curl_handle || !image_path || !device_uid || !cc_session.session_initialized) { - return 0; - } - - // 2. Build URL - char upload_url[CC_URL_SIZE]; - snprintf(upload_url, sizeof(upload_url), - "%s/devices/%s/settings/lcd/lcd/images?log=false", - config->daemon_address, device_uid); - - // 3. Build multipart form - curl_mime *form = curl_mime_init(cc_session.curl_handle); - - // Add mode field - curl_mimepart *field = curl_mime_addpart(form); - curl_mime_name(field, "mode"); - curl_mime_data(field, "image", CURL_ZERO_TERMINATED); - - // Add brightness field - char brightness_str[8]; - snprintf(brightness_str, sizeof(brightness_str), "%d", config->lcd_brightness); - field = curl_mime_addpart(form); - curl_mime_name(field, "brightness"); - curl_mime_data(field, brightness_str, CURL_ZERO_TERMINATED); - - // Add orientation field - char orientation_str[8]; - snprintf(orientation_str, sizeof(orientation_str), "%d", config->lcd_orientation); - field = curl_mime_addpart(form); - curl_mime_name(field, "orientation"); - curl_mime_data(field, orientation_str, CURL_ZERO_TERMINATED); - - // Add image file - field = curl_mime_addpart(form); - curl_mime_name(field, "images[]"); - curl_mime_filedata(field, image_path); - curl_mime_type(field, "image/png"); - - // 4. Configure CURL - curl_easy_setopt(cc_session.curl_handle, CURLOPT_URL, upload_url); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_MIMEPOST, form); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, "PUT"); - - // 5. Send request - CURLcode res = curl_easy_perform(cc_session.curl_handle); - long http_response_code = -1; - curl_easy_getinfo(cc_session.curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); - - // 6. Cleanup - curl_mime_free(form); - - // 7. Check result - return (res == CURLE_OK && http_response_code == 200); -} +Fields: + mode: "image" + brightness: "80" + orientation: "0" + images[]: ``` -**Helper Functions**: -- `build_lcd_upload_form()`: Construct multipart form -- `add_mime_field()`: Add string field with error checking -- `add_image_file_field()`: Add PNG file with MIME type -- `configure_lcd_upload_curl()`: Set CURL options -- `cleanup_lcd_upload_curl()`: Reset CURL state - -**Error Handling**: -- Validates session initialization -- Checks file existence (via `curl_mime_filedata()`) -- Logs CURL errors and HTTP codes -- Returns 0 on failure, 1 on success - -#### 5. Session Cleanup - -**Function**: `cleanup_coolercontrol_session(void)` - -**Purpose**: Clean shutdown with resource deallocation - -**Steps**: -1. Cleanup CURL easy handle -2. Cleanup libcurl global state -3. Mark session as uninitialized -4. Clear the cached Bearer header -5. Set cleanup flag to prevent double-cleanup +### Shutdown Image (CC4) -**Implementation**: -```c -void cleanup_coolercontrol_session(void) -{ - static int cleanup_done = 0; - if (cleanup_done) - return; - - // Cleanup CURL handle - if (cc_session.curl_handle) { - curl_easy_cleanup(cc_session.curl_handle); - cc_session.curl_handle = NULL; - } - - // Global CURL cleanup - curl_global_cleanup(); +Called once at startup. CC4 stores it server-side and displays it when CoolerControl stops. - // Mark as uninitialized - cc_session.session_initialized = 0; - cc_session.access_token[0] = '\0'; - cleanup_done = 1; -} +``` +PUT {address}/devices/{uid}/settings/lcd/lcd/shutdown-image ``` -**Called By**: `main()` via `atexit()` or signal handler - -#### 6. CURL Write Callback - -**Function**: `write_callback(const void *contents, size_t size, size_t nmemb, http_response *response)` - -**Purpose**: Receive HTTP response data from libcurl - -**Algorithm**: -1. Calculate received size: `realsize = size * nmemb` -2. Check if buffer needs reallocation -3. If needed, grow buffer exponentially (1.5x or exact) -4. Copy new data to buffer using `memmove()` -5. Update size and null-terminate -6. Return received size (or 0 on error) - -**Memory Safety**: -- Prevents buffer overflow via size checking -- Exponential growth reduces reallocation frequency -- Always null-terminates for string safety - -### File: `src/srv/cc_main.h` - -**Purpose**: Public API declarations for session management +### HTTP Response Buffer -**Key Definitions**: ```c -// Response buffer typedef struct http_response { char *data; size_t size; size_t capacity; } http_response; - -// Buffer management -int cc_init_response_buffer(http_response *response, size_t initial_capacity); -void cc_cleanup_response_buffer(http_response *response); - -// Session management -int init_coolercontrol_session(const struct Config *config); -int is_session_initialized(void); -void cleanup_coolercontrol_session(void); - -// LCD upload -int send_image_to_lcd(const struct Config *config, const char *image_path, const char *device_uid); - -// CURL callback -size_t write_callback(const void *contents, size_t size, size_t nmemb, http_response *response); ``` -**Constants**: -```c -#define CC_COOKIE_SIZE 512 -#define CC_UID_SIZE 128 -#define CC_URL_SIZE 512 -#define CC_USERPWD_SIZE 128 -#define CC_MAX_SAFE_ALLOC_SIZE (SIZE_MAX / 2) -``` +Growth strategy: `new_capacity = max(required_size, capacity * 3/2)` --- -## Module cc_conf - -### Purpose -Device information caching, circular display detection, and configuration utilities. - -### File: `src/srv/cc_conf.c` (488 lines) +## cc_conf — Device Cache -### Key Components - -#### 1. Device Cache - -**Purpose**: Store device info once to avoid repeated API calls +### Cache Structure ```c static struct { @@ -419,289 +111,53 @@ static struct { } device_cache = {0}; ``` -**Rationale**: -- Device properties don't change during runtime -- Reduces API overhead (every render cycle) -- Improves performance and reliability +Populated once at startup via `GET /devices`. Device properties don't change at runtime. -**Access Function**: `get_cached_lcd_device_data()` -- Returns cached data if available -- Initializes cache on first call -- Thread-safe (single-threaded daemon) +### Public API -#### 2. Device Detection +| Function | Purpose | +|----------|---------| +| `init_device_cache(config)` | Fetch + cache device info | +| `get_cached_lcd_device_data(...)` | Read cached UID, name, dimensions | +| `update_config_from_device(config)` | Set width/height if 0 in config | +| `is_circular_display_device(name, w, h)` | Detect display shape | -**Function**: `init_device_cache(const Config *config)` +### Device JSON Structure -**Purpose**: Fetch device list and extract CoolerControl LCD device info - -**API Call**: -``` -GET {daemon_address}/devices -``` - -**Response Structure**: ```json { - "devices": [ - { - "uid": "1234-5678-abcd", - "name": "NZXT Kraken Elite", - "type": "Liquidctl", - "info": { - "channels": { - "lcd": { - "lcd_info": { - "width": 640, - "height": 640 - } - } + "devices": [{ + "uid": "1234-5678-abcd", + "name": "NZXT Kraken Elite", + "type": "Liquidctl", + "info": { + "channels": { + "lcd": { + "lcd_info": { "screen_width": 640, "screen_height": 640 } } } } - ] -} -``` - -**Implementation**: -```c -int init_device_cache(const Config *config) -{ - if (device_cache.initialized) - return 1; - - // Build URL - char url[256]; - snprintf(url, sizeof(url), "%s/devices", config->daemon_address); - - // Initialize response buffer - http_response response = {0}; - cc_init_response_buffer(&response, 16384); - - // Send request - CURL *curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - - CURLcode res = curl_easy_perform(curl); - - // Parse JSON response - if (res == CURLE_OK) { - parse_lcd_device_data(response.data, - device_cache.device_uid, - sizeof(device_cache.device_uid), - &device_cache.screen_width, - &device_cache.screen_height, - device_cache.device_name, - sizeof(device_cache.device_name)); - device_cache.initialized = 1; - } - - // Cleanup - cc_cleanup_response_buffer(&response); - curl_easy_cleanup(curl); - - return device_cache.initialized; -} -``` - -#### 3. JSON Parsing - -**Function**: `parse_lcd_device_data(const char *json, ...)` - -**Purpose**: Extract device information from devices JSON response - -**Algorithm**: -1. Parse JSON string with jansson -2. Get "devices" array -3. Iterate through devices -4. Check device type ("Liquidctl") -5. Extract UID from "uid" field -6. Extract name from "name" field -7. Navigate to "info.channels.lcd.lcd_info" -8. Extract width and height -9. Return first matching device - -**Helper Functions**: -- `extract_device_type_from_json()`: Get device type string -- `extract_device_uid()`: Get UID string -- `extract_device_name()`: Get device name -- `get_lcd_info_from_device()`: Navigate JSON to lcd_info -- `extract_lcd_dimensions()`: Get width/height from lcd_info -- `has_usable_device_uid()`: Verify that the device exposes a usable UID - -**Error Handling**: -- Validates JSON structure at each level -- Returns 0 if any field is missing -- Logs errors for debugging - -#### 4. Circular Display Detection - -**Function**: `is_circular_display_device(const char *device_name, int screen_width, int screen_height)` - -**Purpose**: Determine if device has circular LCD display - -**Detection Logic**: - -```c -int is_circular_display_device(const char *device_name, int screen_width, int screen_height) -{ - if (!device_name) - return 0; - - // NZXT Kraken detection - const int is_kraken = (strstr(device_name, "Kraken") != NULL); - - if (is_kraken) { - // Elite models with >240x240 are circular - if (screen_width > 240 || screen_height > 240) { - return 1; - } - return 0; // 240x240 and smaller are rectangular - } - - // Known circular devices list - const char *circular_devices[] = { - "Corsair iCUE LINK" - // Add more models here - }; - - for (size_t i = 0; i < num_circular; i++) { - if (strstr(device_name, circular_devices[i]) != NULL) { - return 1; - } - } - - return 0; -} -``` - -**NZXT Kraken Rules**: -- **240x240**: Rectangular (e.g., Kraken Z53) -- **>240x240**: Circular (e.g., Kraken Elite 280/360 with 640x640 display) - -**Display Shape Override (v1.96+)**: - -**Recommended Method:** `display_shape` config parameter (highest priority): -```ini -[display] -shape = auto # or "rectangular" or "circular" -``` - -**Legacy Method:** `force_display_circular` flag (backwards compatibility): -```ini -[display] -force_circular = true -``` - -**Priority System:** -1. `display_shape` config (manual override) -2. `force_display_circular` flag (legacy) -3. Automatic device detection (default) - -#### 5. Configuration Update - -**Function**: `update_config_from_device(Config *config)` - -**Purpose**: Update config with device screen dimensions (only if not set in `config.json`) - -**Logic**: -```c -int update_config_from_device(Config *config) -{ - if (!config) - return 0; - - // Only update if config.json values are 0 (auto-detect) - if (config->display_width == 0) { - config->display_width = device_cache.screen_width; - log_message(LOG_INFO, "Updated display width to %d from device", - device_cache.screen_width); - } - - if (config->display_height == 0) { - config->display_height = device_cache.screen_height; - log_message(LOG_INFO, "Updated display height to %d from device", - device_cache.screen_height); - } - - return 1; + }] } ``` -**Use Case**: Auto-detect display resolution when user doesn't specify in `config.json` +### Circular Display Detection -#### 6. Safe String Copy +- NZXT Kraken ≤240×240 → rectangular +- NZXT Kraken >240×240 → circular +- Other devices: check database, default rectangular -**Function**: `cc_safe_strcpy(char *restrict dest, size_t dest_size, const char *restrict src)` +Override via `config.json`: `"shape": "rectangular"` or `"circular"` -**Purpose**: Bounds-checked string copy with null-termination guarantee +### cc_safe_strcpy -**Implementation**: -```c -int cc_safe_strcpy(char *restrict dest, size_t dest_size, const char *restrict src) -{ - if (!dest || !src || dest_size == 0) - return -1; - - for (size_t i = 0; i < dest_size - 1; i++) { - dest[i] = src[i]; - if (src[i] == '\0') - return 0; - } - - dest[dest_size - 1] = '\0'; - return 0; -} -``` - -**Advantages**: -- Prevents buffer overflow -- Always null-terminates -- Simple and portable -- No dependency on platform-specific functions - -### File: `src/srv/cc_conf.h` - -**Purpose**: Public API for device caching and detection - -**Key Functions**: -```c -// Device cache -int init_device_cache(const struct Config *config); -int get_cached_lcd_device_data(const struct Config *config, char *device_uid, - size_t uid_size, char *device_name, size_t name_size, - int *screen_width, int *screen_height); - -// Configuration -int update_config_from_device(struct Config *config); - -// Detection -int is_circular_display_device(const char *device_name, int screen_width, int screen_height); - -// Utilities -int cc_safe_strcpy(char *restrict dest, size_t dest_size, const char *restrict src); -const char *extract_device_type_from_json(const json_t *dev); -``` - -**Constants**: -```c -#define CC_NAME_SIZE 128 -``` +Bounds-checked string copy with guaranteed null-termination. Use instead of `strcpy`/`strncpy`. --- -## Module cc_sensor - -### Purpose -Retrieve CPU and GPU temperature data from CoolerControl status endpoint. +## cc_sensor — Temperature -### File: `src/srv/cc_sensor.c` (290 lines) - -### Key Components - -#### 1. Temperature Data Structure +### Data Structure ```c typedef struct { @@ -710,856 +166,60 @@ typedef struct { } monitor_sensor_data_t; ``` -**Usage**: -```c -monitor_sensor_data_t data = {0}; -get_temperature_monitor_data(&config, &data); -printf("CPU: %.1f°C, GPU: %.1f°C\n", data.temp_cpu, data.temp_gpu); -``` - -#### 2. Temperature Retrieval - -**Function**: `get_temperature_monitor_data(const Config *config, monitor_sensor_data_t *data)` - -**Purpose**: High-level API to fetch temperature data +### API Request -**Implementation**: -```c -int get_temperature_monitor_data(const Config *config, monitor_sensor_data_t *data) -{ - if (!config || !data) - return 0; - - return get_temperature_data(config, &data->temp_cpu, &data->temp_gpu); -} ``` - -**Internal Flow**: Delegates to `get_temperature_data()` - -#### 3. Status API Request - -**Function**: `get_temperature_data(const Config *config, float *temp_cpu, float *temp_gpu)` - -**API Call**: -``` -POST {daemon_address}/status +POST {address}/status Content-Type: application/json -{ - "all": false, - "since": "1970-01-01T00:00:00.000Z" -} +{"all": false, "since": "1970-01-01T00:00:00.000Z"} ``` -**Response Structure**: -```json -{ - "devices": [ - { - "type": "CPU", - "status_history": [ - { - "temps": [ - { - "name": "temp1", - "temp": 45.0 - } - ] - } - ] - }, - { - "type": "GPU", - "status_history": [ - { - "temps": [ - { - "name": "GPU Core", - "temp": 52.0 - } - ] - } - ] - } - ] -} -``` +### Response -**Implementation**: -```c -static int get_temperature_data(const Config *config, float *temp_cpu, float *temp_gpu) +```json { - // Initialize outputs - *temp_cpu = 0.0f; - *temp_gpu = 0.0f; - - // Build URL - char url[256]; - snprintf(url, sizeof(url), "%s/status", config->daemon_address); - - // Initialize response buffer - http_response response = {0}; - cc_init_response_buffer(&response, 8192); - - // Configure request - CURL *curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - - // POST data - const char *post_data = "{\"all\":false,\"since\":\"1970-01-01T00:00:00.000Z\"}"; - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); - - // Set headers - struct curl_slist *headers = NULL; - headers = curl_slist_append(headers, "accept: application/json"); - headers = curl_slist_append(headers, "content-type: application/json"); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - // Send request - CURLcode res = curl_easy_perform(curl); - - // Parse response - if (res == CURLE_OK) { - parse_temperature_data(response.data, temp_cpu, temp_gpu); - } - - // Cleanup - cc_cleanup_response_buffer(&response); - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - return (res == CURLE_OK); + "devices": [{ + "type": "CPU", + "status_history": [{ + "temps": [{ "name": "temp1", "temp": 45.0 }] + }] + }] } ``` -#### 4. JSON Temperature Parsing - -**Function**: `parse_temperature_data(const char *json, float *temp_cpu, float *temp_gpu)` - -**Purpose**: Extract CPU and GPU temperatures from status JSON - -**Algorithm**: -1. Parse JSON with jansson -2. Get "devices" array -3. For each device: - - Get device type (CPU/GPU) - - Get status_history array - - Get latest status (last element) - - Get temps array - - Search for appropriate sensor name - - Extract temperature value - - Validate range (-50°C to 150°C) -4. Return when both CPU and GPU found - -**Sensor Name Matching**: -- **CPU**: `temp1` (standard Linux sensor name) -- **GPU**: Contains "GPU" or "gpu" (varies by manufacturer) - -**Helper Function**: `extract_device_temperature()` - -```c -static float extract_device_temperature(const json_t *device, const char *device_type) -{ - // Get status history - const json_t *status_history = json_object_get(device, "status_history"); - size_t history_count = json_array_size(status_history); - - // Get latest status - const json_t *last_status = json_array_get(status_history, history_count - 1); - - // Get temperatures array - const json_t *temps = json_object_get(last_status, "temps"); - - // Search for matching sensor - size_t temp_count = json_array_size(temps); - for (size_t i = 0; i < temp_count; i++) { - const json_t *temp_entry = json_array_get(temps, i); - const char *sensor_name = json_string_value(json_object_get(temp_entry, "name")); - float temperature = json_number_value(json_object_get(temp_entry, "temp")); - - // Validate range - if (temperature < -50.0f || temperature > 150.0f) - continue; - - // Check sensor name - if (strcmp(device_type, "CPU") == 0 && strcmp(sensor_name, "temp1") == 0) { - return temperature; - } - else if (strcmp(device_type, "GPU") == 0 && - (strstr(sensor_name, "GPU") || strstr(sensor_name, "gpu"))) { - return temperature; - } - } - - return 0.0f; -} -``` - -**Error Handling**: -- Returns 0.0f on missing data -- Validates JSON structure at each level -- Range checks temperature values -- Logs parsing errors - -### File: `src/srv/cc_sensor.h` - -**Purpose**: Public API for temperature data retrieval - -**Key Definitions**: -```c -// Data structure -typedef struct { - float temp_cpu; - float temp_gpu; -} monitor_sensor_data_t; - -// High-level API -int get_temperature_monitor_data(const struct Config *config, monitor_sensor_data_t *data); -``` +Sensor matching: CPU = `temp1`, GPU = name contains "GPU"/"gpu". +Validation: -50°C to 150°C range. --- -## API Communication Flow - -### Initialization Phase - -``` -main() startup - ↓ -1. load_configuration() - ↓ -2. init_coolercontrol_session(&config) - │ - └─→ Build Authorization: Bearer cc_ - ↓ -3. init_device_cache(&config) - │ - ├─→ GET /devices - │ Response: Device list JSON - │ - ├─→ Parse JSON, find first LCD-capable CoolerControl device - │ - └─→ Cache: UID, name, width, height - ↓ -4. update_config_from_device(&config) - │ - └─→ Update display_width/height if 0 in config - ↓ -5. Ready for main loop -``` - -### Main Loop Phase - -``` -while (running) - ↓ -1. get_temperature_monitor_data(&config, &data) - │ - ├─→ POST /status - │ Content-Type: application/json - │ Body: {"all":false,"since":"1970-01-01T00:00:00.000Z"} - │ Cookie: {session_cookie} - │ - ├─→ Response: Device status JSON with temps - │ - └─→ Parse: Extract temp_cpu and temp_gpu - ↓ -2. draw_display_image(&config) - │ - ├─→ render_dual_display() or render_circle_display() - │ │ - │ ├─→ Cairo rendering - │ │ - │ └─→ Write PNG: /etc/coolercontrol/plugins/coolerdash/coolerdash.png - │ - └─→ send_image_to_lcd(&config, png_path, device_uid) - │ - ├─→ PUT /devices/{uid}/settings/lcd/lcd/images?log=false - │ Content-Type: multipart/form-data - │ Cookie: {session_cookie} - │ - │ Fields: - │ - mode: "image" - │ - brightness: "100" - │ - orientation: "0" - │ - images[]: {PNG file data} - │ - └─→ Response: 200 OK - ↓ -3. sleep(update_interval) - ↓ -4. Loop back to step 1 -``` - -### Shutdown Phase - -``` -Signal received (SIGTERM/SIGINT) or exit() - ↓ -cleanup_coolercontrol_session() - │ - ├─→ curl_easy_cleanup() - │ - ├─→ curl_global_cleanup() - │ - ├─→ unlink(/tmp/coolerdash_cookie_{PID}.txt) - │ - └─→ session_initialized = 0 - ↓ -Program exit -``` - ---- +## Testing API Manually -## Data Structures - -### Config Structure (Relevant Fields) - -```c -struct Config { - // CoolerControl connection - char daemon_address[256]; // e.g., "http://127.0.0.1:11987" - char access_token[64]; // Bearer token - - // Display settings - uint16_t display_width; // e.g., 640 (auto-detected if 0) - uint16_t display_height; // e.g., 640 - char display_shape[16]; // "auto", "rectangular", or "circular" (v1.96+) - int force_display_circular; // Legacy override (deprecated) - - // LCD settings - int lcd_brightness; // 0-100 - int lcd_orientation; // 0, 90, 180, 270 - - // Display mode - char display_mode[16]; // "dual" or "circle" - - // Paths - char paths_image_coolerdash[512]; // PNG output path -}; -``` - -### Device Cache - -```c -static struct { - int initialized; // 0 = not cached, 1 = cached - char device_uid[128]; // e.g., "1234-5678-abcd" - char device_name[CC_NAME_SIZE]; // e.g., "NZXT Kraken Elite" - int screen_width; // e.g., 640 - int screen_height; // e.g., 640 -} device_cache; -``` - -### Session State - -```c -typedef struct { - CURL *curl_handle; // Persistent CURL handle - char access_token[CC_BEARER_HEADER_SIZE]; // Authorization header - int session_initialized; // 0 = not initialized, 1 = ready -} CoolerControlSession; - -static CoolerControlSession cc_session; -``` - -### HTTP Response Buffer - -```c -typedef struct http_response { - char *data; // Dynamic buffer - size_t size; // Current data size - size_t capacity; // Allocated capacity -} http_response; -``` - -### Temperature Data - -```c -typedef struct { - float temp_cpu; // CPU temperature (°C) - float temp_gpu; // GPU temperature (°C) -} monitor_sensor_data_t; -``` - ---- - -## Error Handling - -### Return Value Convention - -- **Success**: 1 (true) -- **Failure**: 0 (false) -- **Error**: Negative values (rare, mostly in legacy code) - -### Logging Levels - -```c -LOG_ERROR - Critical failures (API unreachable, auth failed) -LOG_WARNING - Recoverable issues (sensor missing, invalid data) -LOG_INFO - Verbose mode info (API responses, cache hits) -LOG_STATUS - Normal operation status (upload success) -``` - -### Common Error Scenarios - -#### 1. Authentication Failure - -**Symptom**: `init_coolercontrol_session()` returns 0 - -**Causes**: -- Missing or invalid access token -- CoolerControl daemon not running -- Daemon address unreachable - -**Debugging**: -```c -log_message(LOG_ERROR, - "CoolerControl access token missing; token-only authentication is required"); -``` - -**Resolution**: -- Verify daemon is running: `systemctl status coolercontrol` -- Check `access_token` in `config.json` -- Test connection: `curl http://127.0.0.1:11987/devices` - -#### 2. Device Not Found - -**Symptom**: `init_device_cache()` returns 0 - -**Causes**: -- No LCD-capable CoolerControl device connected -- Device not detected by CoolerControl -- Wrong device type in JSON - -**Debugging**: -```c -log_message(LOG_ERROR, "No LCD device found in API response"); -``` - -**Resolution**: -- Check CoolerControl GUI for device list -- Verify Liquidctl support: `liquidctl list` -- Ensure device permissions (udev rules) - -#### 3. Temperature Data Missing - -**Symptom**: `temp_cpu` or `temp_gpu` is 0.0 - -**Causes**: -- Sensor not available in CoolerControl -- Sensor name mismatch -- Device disconnected - -**Debugging**: -```c -log_message(LOG_WARNING, "CPU temperature not found in status response"); -``` - -**Resolution**: -- Check CoolerControl dashboard for sensor visibility -- Verify sensor names in JSON response -- Ensure drivers loaded (lm-sensors, nvidia-smi) - -#### 4. LCD Upload Failure - -**Symptom**: `send_image_to_lcd()` returns 0 - -**Causes**: -- PNG file not found -- Device UID invalid -- Session cookie expired - -**Debugging**: -```c -log_message(LOG_ERROR, "LCD upload failed: CURL code %d, HTTP code %ld", res, http_response_code); -if (response.data && response.size > 0) { - log_message(LOG_ERROR, "Server response: %s", response.data); -} -``` - -**Resolution**: -- Verify PNG exists: `ls -l /tmp/coolerdash.png` -- Check device UID matches cache -- Re-authenticate if session lost - ---- - -## Best Practices - -### 1. Resource Management - -**Always pair initialization with cleanup**: -```c -// Buffer -http_response response = {0}; -cc_init_response_buffer(&response, 8192); -// ... use response ... -cc_cleanup_response_buffer(&response); - -// CURL -CURL *curl = curl_easy_init(); -// ... use curl ... -curl_easy_cleanup(curl); - -// Headers -struct curl_slist *headers = NULL; -headers = curl_slist_append(headers, "Content-Type: application/json"); -// ... use headers ... -curl_slist_free_all(headers); -``` - -### 2. Session Handling - -**Check session before API calls**: -```c -if (!is_session_initialized()) { - log_message(LOG_ERROR, "Session not initialized"); - return 0; -} -``` - -**Use persistent session for all requests**: -- Don't create new CURL handles per request -- Reuse `cc_session.curl_handle` for performance -- Cookie persistence handles re-authentication - -### 3. JSON Parsing - -**Validate structure at each level**: -```c -json_t *root = json_loads(json_string, 0, &error); -if (!root) { - log_message(LOG_ERROR, "JSON parse error: %s", error.text); - return 0; -} - -const json_t *devices = json_object_get(root, "devices"); -if (!devices || !json_is_array(devices)) { - json_decref(root); - return 0; -} - -// ... continue parsing ... - -json_decref(root); // Always cleanup -``` - -**Always decref JSON objects**: -```c -json_t *root = json_loads(...); -// ... use root ... -json_decref(root); // Free memory -``` - -### 4. Error Logging - -**Provide actionable context**: -```c -// Bad -log_message(LOG_ERROR, "Failed"); - -// Good -log_message(LOG_ERROR, "Failed to upload LCD image: HTTP %ld, device UID: %s", - http_code, device_uid); -``` - -**Include CURL error details**: -```c -CURLcode res = curl_easy_perform(curl); -if (res != CURLE_OK) { - log_message(LOG_ERROR, "CURL request failed: %s", curl_easy_strerror(res)); -} -``` - -### 5. String Safety - -**Use bounds-checked functions**: -```c -// Prefer cc_safe_strcpy over strcpy/strncpy -cc_safe_strcpy(dest, sizeof(dest), src); - -// Always validate snprintf -int written = snprintf(buffer, sizeof(buffer), format, args); -if (written < 0 || (size_t)written >= sizeof(buffer)) { - log_message(LOG_ERROR, "String truncated"); - return 0; -} -``` - -### 6. Memory Allocation - -**Check allocation success**: -```c -char *buffer = malloc(size); -if (!buffer) { - log_message(LOG_ERROR, "Memory allocation failed: %zu bytes", size); - return 0; -} -``` - -**Prevent overflow in size calculations**: -```c -if (initial_capacity > CC_MAX_SAFE_ALLOC_SIZE) { - log_message(LOG_ERROR, "Requested size too large: %zu", initial_capacity); - return 0; -} -``` - -### 7. HTTPS Support - -**Enable SSL verification**: -```c -if (strncmp(config->daemon_address, "https://", 8) == 0) { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); -} -``` - -**Certificate path** (optional): -```c -curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt"); -``` - ---- - -## Troubleshooting - -### Debugging API Communication - -**Enable CURL verbose output**: -```c -curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); -``` - -**Capture HTTP headers**: -```c -static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) -{ - printf("Header: %s", buffer); - return size * nitems; -} - -curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); -``` - -**Dump response body**: -```c -if (response.data && response.size > 0) { - FILE *f = fopen("/tmp/coolerdash_api_response.json", "w"); - fwrite(response.data, 1, response.size, f); - fclose(f); - log_message(LOG_INFO, "API response saved to /tmp/coolerdash_api_response.json"); -} -``` - -### Testing API Calls Manually - -**Test authentication**: ```bash +# Devices curl http://127.0.0.1:11987/devices \ - -H "Authorization: Bearer cc_" \ - -v -``` + -H "Authorization: Bearer cc_" | jq -**Test device list**: -```bash -curl -X GET http://127.0.0.1:11987/devices \ - -b cookies.txt \ - -H "Accept: application/json" | jq -``` - -**Test temperature data**: -```bash +# Temperature curl -X POST http://127.0.0.1:11987/status \ - -b cookies.txt \ - -H "Content-Type: application/json" \ - -d '{"all":false,"since":"1970-01-01T00:00:00.000Z"}' | jq -``` + -H "Content-Type: application/json" \ + -H "Authorization: Bearer cc_" \ + -d '{"all":false,"since":"1970-01-01T00:00:00.000Z"}' | jq -**Test LCD upload**: -```bash +# LCD upload curl -X PUT "http://127.0.0.1:11987/devices/{UID}/settings/lcd/lcd/images?log=false" \ - -b cookies.txt \ - -F "mode=image" \ - -F "brightness=100" \ - -F "orientation=0" \ - -F "images[]=@/tmp/coolerdash.png;type=image/png" \ - -v -``` - -### Common Issues - -#### Issue: "Session not initialized" - -**Check**: -```bash -systemctl status coolercontrol -curl -I http://127.0.0.1:11987 -``` - -**Fix**: Start CoolerControl daemon -```bash -systemctl start coolercontrol -``` - -#### Issue: "No LCD device found" - -**Check devices**: -```bash -curl http://127.0.0.1:11987/devices | jq '.devices[] | {type, name}' -``` - -**Verify Liquidctl**: -```bash -sudo liquidctl list -``` - -#### Issue: "Temperature always 0.0" - -**Check sensor names**: -```bash -curl -X POST http://127.0.0.1:11987/status \ - -H "Content-Type: application/json" \ - -d '{"all":false,"since":"1970-01-01T00:00:00.000Z"}' | \ -jq '.devices[] | select(.type=="CPU" or .type=="GPU") | .status_history[0].temps' -``` - -**Adjust sensor name matching** in `extract_device_temperature()` if needed. - -#### Issue: "LCD upload fails with HTTP 401" - -**Cause**: Session cookie expired - -**Fix**: Session re-initialization handled automatically, but check: -```bash -ls -la /tmp/coolerdash_cookie_*.txt -cat /tmp/coolerdash_cookie_*.txt + -H "Authorization: Bearer cc_" \ + -F "mode=image" -F "brightness=80" -F "orientation=0" \ + -F "images[]=@coolerdash.png;type=image/png" ``` --- -## Performance Considerations - -### API Call Frequency - -**Current Setup** (default 2-second interval): -- Temperature data: POST /status every 2s -- LCD upload: PUT /devices/.../images every 2s -- Device list: GET /devices once at startup (cached) - -**Optimization**: -- Device cache eliminates repeated /devices calls -- Persistent CURL session reuses HTTP connection -- Shared Bearer header avoids rebuilding auth state on every request - -### Memory Usage - -**Typical Allocations**: -- Response buffers: 4-16KB (dynamic) -- Device cache: ~400 bytes (static) -- Session state: ~600 bytes (static) -- JSON parsing: Temporary (freed immediately) - -**Buffer Sizing**: -- `/devices` response: 8-16KB (many devices) -- `/status` response: 4-8KB (temperature data) -- Upload response: 4KB (minimal) - -### Network Overhead - -**Per Update Cycle** (2 seconds): -- Request: ~500 bytes (POST /status) -- Response: ~2-4KB (JSON) -- Upload request: ~50-100KB (PNG + multipart) -- Upload response: ~200 bytes - -**Total**: ~50-104KB per cycle = ~25-52KB/s average - ---- - -## Future Enhancements - -### Potential Improvements - -1. **Asynchronous API Calls**: Use libcurl multi interface for parallel requests -2. **Connection Pooling**: Reuse connections more efficiently -3. **Compression**: Enable gzip for JSON responses -4. **WebSocket Support**: Real-time temperature streaming instead of polling -5. **Retry Logic**: Automatic retry with exponential backoff on transient failures -6. **Response Caching**: Skip LCD upload if image hasn't changed -7. **Multiple Devices**: Support multiple LCD devices simultaneously -8. **Device Hot-Plug**: Detect device connection/disconnection and re-initialize - -### API Version Compatibility - -**Current**: CoolerControl v1.x REST API - -**Future**: If CoolerControl API changes: -- Add version detection: GET /version -- Conditional logic for different API versions -- Deprecation warnings for old endpoints - ---- - -## Conclusion - -The CoolerControl API integration provides a robust, efficient interface for: -- **Authentication**: Bearer token via CoolerControl Access Protection -- **Device Detection**: Automatic CoolerControl LCD device discovery and caching -- **Temperature Monitoring**: Real-time CPU/GPU data retrieval -- **LCD Upload**: Multipart image upload with brightness/orientation control -- **Error Handling**: Comprehensive validation and logging - -### Key Design Principles - -- **Caching**: Device information cached to minimize API calls -- **Persistence**: Reusable CURL session for performance -- **Safety**: Bounds-checked strings, validated allocations -- **Modularity**: Separate concerns (auth, config, sensors) -- **Reliability**: Extensive error checking and logging - -### Integration Example - -```c -// Complete workflow -Config config = load_configuration(); - -// Initialize API -if (!init_coolercontrol_session(&config)) { - log_message(LOG_ERROR, "Failed to connect to CoolerControl"); - return 1; -} - -if (!init_device_cache(&config)) { - log_message(LOG_ERROR, "No compatible device found"); - cleanup_coolercontrol_session(); - return 1; -} - -update_config_from_device(&config); - -// Main loop -while (running) { - monitor_sensor_data_t data = {0}; - if (get_temperature_monitor_data(&config, &data)) { - // Render and upload handled by display modules - draw_display_image(&config); - } - sleep(2); -} - -// Cleanup -cleanup_coolercontrol_session(); -``` - -For questions or contributions, see `CONTRIBUTING.md`. - ---- +## Troubleshooting -**Version**: 1.0 -**Last Updated**: November 6, 2025 -**Authors**: damachine +| Problem | Check | +|---------|-------| +| Session init fails | `systemctl status coolercontrold`, verify `access_token` | +| No LCD device found | `curl .../devices \| jq`, check for `"type": "Liquidctl"` | +| Temperature 0.0 | Check sensor names in `/status` response | +| Upload fails (401) | Token invalid or missing | diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 2905344..b6d5ab0 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -1,1269 +1,167 @@ -# CoolerDash Developer Documentation +# CoolerDash Developer Guide -**Language:** C99 | **Platform:** Linux x86-64-v3 | **License:** MIT -**Author:** damachine (damachin3@proton.me) -**Repository:** https://github.com/damachine/coolerdash - ---- - -## Table of Contents - -1. [Project Overview](#project-overview) -2. [Architecture](#architecture) -3. [Module Structure](#module-structure) -4. [Build System](#build-system) -5. [Configuration System](#configuration-system) -6. [API Integration](#api-integration) -7. [Rendering Pipeline](#rendering-pipeline) -8. [Function Reference](#function-reference) -9. [Development Guidelines](#development-guidelines) -10. [Testing & Debugging](#testing--debugging) - ---- - -## Project Overview - -### Purpose - -CoolerDash extends the LCD functionality of [CoolerControl](https://gitlab.com/coolercontrol/coolercontrol) for Linux systems, targeting CoolerControl-managed LCD displays such as AIO pump screens and similar supported devices. It provides real-time sensor visualization with customizable UI elements. - -### Key Features - -- **Real-time Temperature Monitoring:** CPU/GPU/liquid sensor data via CoolerControl REST API -- **Adaptive Display Rendering:** Automatic circular/rectangular display detection -- **Customizable UI:** Full color/layout/font/sensor-slot configuration via `config.json` -- **Authentication:** Bearer Token via CoolerControl Access Protection -- **Shutdown Image:** Registered with CC4 at startup — CC handles display on daemon stop -- **Efficient Caching:** One-time device information retrieval at startup -- **CoolerControl Plugin:** Managed by `cc-plugin-coolerdash.service`, no separate systemd service needed - -### System Requirements - -- **OS:** Linux (systemd-based) -- **Architecture:** x86-64-v3 (Intel Haswell+ / AMD Excavator+) -- **Dependencies:** - - `cairo` — PNG generation - - `jansson` — JSON parsing (config + API) - - `libcurl-gnutls` — HTTP client - - `ttf-roboto` — Font rendering -- **Required Service:** CoolerControl >=4.x +**C99** | **Linux x86-64-v3** | **MIT License** +Author: damachine (damachin3@proton.me) +Repository: https://github.com/damachine/coolerdash --- ## Architecture -### High-Level Flow - ``` -┌─────────────────────────────────────────────────────────────────┐ -│ main.c │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 1. Configuration Loading (device/config.c) │ │ -│ │ 2. Session Initialization (cc_main.c) │ │ -│ │ └─ Bearer Token header │ │ -│ │ 3. Device Cache Setup (cc_conf.c) │ │ -│ │ 4. Shutdown Image Registration (cc_main.c, CC4 only) │ │ -│ │ 5. Main Loop (configurable interval) │ │ -│ │ ├─ Temperature Reading (cc_sensor.c) │ │ -│ │ ├─ Image Rendering (display.c → dual.c|circle.c) │ │ -│ │ └─ LCD Upload (cc_main.c) │ │ -│ │ 6. Signal Handling (SIGTERM/SIGINT → graceful stop) │ │ -│ │ 7. Cleanup (session + image files) │ │ -│ └───────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ ↓ ↓ - ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ - │ device/ │ │ srv/ │ │ mods/ │ - │ Configuration │ │ CoolerControl │ │ Rendering │ - │ Management │ │ API Client │ │ Engine │ - └─────────────────┘ └──────────────────┘ └─────────────────┘ - config.c/h cc_main.c/h display.c/h - cc_conf.c/h dual.c/h - cc_sensor.c/h circle.c/h +main.c + ├─ Configuration loading (device/config.c) + ├─ Session init + auth (srv/cc_main.c) + ├─ Device cache setup (srv/cc_conf.c) + ├─ Shutdown image register (srv/cc_main.c, CC4) + ├─ Main loop + │ ├─ Temperature reading (srv/cc_sensor.c) + │ ├─ Image rendering (mods/display.c → dual.c | circle.c) + │ └─ LCD upload (srv/cc_main.c) + ├─ Signal handling (SIGTERM/SIGINT → graceful stop) + └─ Cleanup (session + image files) ``` -### Design Principles - -1. **Separation of Concerns:** Clear module boundaries (device/srv/mods) -2. **Defense in Depth:** Multi-layer error checking and validation -3. **Resource Safety:** RAII-style cleanup patterns with cleanup guards -4. **Performance Optimization:** Device caching, exponential buffer growth -5. **Security First:** PID file race condition prevention, secure parsing - ---- - ## Module Structure -### Directory Layout - ``` -coolerdash/ -├── src/ -│ ├── main.c # Main daemon entry point (967 lines, 30 functions) -│ ├── device/ # Configuration subsystem -│ │ └── config.c/h # JSON config loader + defaults -│ ├── srv/ # CoolerControl API client -│ │ ├── cc_main.c/h # Session management -│ │ ├── cc_conf.c/h # Device cache, display detection -│ │ └── cc_sensor.c/h # Temperature monitoring -│ └── mods/ # Rendering modules -│ ├── display.c/h # Mode dispatcher, shared helpers -│ ├── dual.c/h # Dual mode (CPU+GPU simultaneous) -│ └── circle.c/h # Circle mode (alternating sensor) -├── etc/ -│ ├── coolercontrol/plugins/coolerdash/config.json # User configuration -│ └── systemd/coolerdash.service ← managed by CoolerControl (cc-plugin-coolerdash.service) -├── docs/ # Documentation -│ ├── config.md # Configuration guide -│ ├── devices.md # Supported hardware -│ └── developer-guide.md # This file -├── Makefile # Build system -└── PKGBUILD # Arch/AUR packaging +src/ +├── main.c # Daemon lifecycle, signal handling, PID management +├── device/ +│ └── config.c/h # JSON config loader + defaults +├── srv/ +│ ├── cc_main.c/h # Session management, auth, LCD upload +│ ├── cc_conf.c/h # Device cache, display detection +│ └── cc_sensor.c/h # Temperature monitoring +└── mods/ + ├── display.c/h # Mode dispatcher + ├── dual.c/h # Dual mode (CPU+GPU simultaneous) + └── circle.c/h # Circle mode (alternating sensor) ``` -### Module Responsibilities - -| Module | Purpose | Key Functions | Lines | Public API | -|--------|---------|---------------|-------|------------| -| **main.c** | Daemon lifecycle | Signal handling, PID management, main loop | — | `main()` | -| **device/config** | Config system | JSON loading, hardcoded defaults | — | `load_plugin_config()` | -| **srv/cc_main** | HTTP session | Login/token auth, LCD upload, shutdown image registration | — | 5 functions | -| **srv/cc_conf** | Device cache | UID/name/dimensions, display shape | — | 4 functions | -| **srv/cc_sensor** | Temperature | CPU/GPU sensor reading | — | 1 function | -| **mods/display** | Mode dispatch | Route to dual/circle, shared Cairo helpers | — | `draw_display_image()` | -| **mods/dual** | Dual rendering | CPU+GPU simultaneous layout | — | `draw_dual_image()` | -| **mods/circle** | Circle rendering | Alternating single-sensor layout | — | `draw_circle_image()` | +| Module | Public API | +|--------|------------| +| main.c | `main()` | +| device/config | `load_plugin_config()` | +| srv/cc_main | `init_coolercontrol_session()`, `is_session_initialized()`, `cleanup_coolercontrol_session()`, `send_image_to_lcd()` | +| srv/cc_conf | `init_device_cache()`, `get_cached_lcd_device_data()`, `update_config_from_device()`, `is_circular_display_device()` | +| srv/cc_sensor | `get_temperature_monitor_data()` | +| mods/display | `draw_display_image()` | +| mods/dual | `draw_dual_image()` | +| mods/circle | `draw_circle_image()` | --- ## Build System -### Makefile Overview - -**Location:** `/Makefile` -**Build Tool:** GNU Make + GCC -**Standard:** C99 with strict warnings - -#### Key Targets - ```bash -make # Standard build (C99, -O2, -march=x86-64-v3) -make clean # Remove build artifacts (build/, bin/) -make install # System installation with dependency checks -make uninstall # Complete removal (service + files) -make debug # Debug build (-g -DDEBUG -fsanitize=address) -make help # Show all available targets +make # C99, -O2, -march=x86-64-v3 +make clean # Remove build artifacts +make debug # Debug build with AddressSanitizer +make install # System installation +make uninstall # Complete removal ``` -#### Compiler Flags - +Compiler flags: ```makefile CFLAGS = -Wall -Wextra -O2 -std=c99 -march=x86-64-v3 -Iinclude \ $(shell pkg-config --cflags cairo jansson libcurl) LIBS = $(shell pkg-config --libs cairo jansson libcurl) -lm ``` -**Optimization Level:** `-O2` (production), `-O0` (debug) -**Architecture:** `x86-64-v3` (AVX2/BMI2 support) -**Warnings:** All enabled (`-Wall -Wextra`) - -#### Build Workflow - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. Dependency Detection (pkg-config) │ -│ ├─ cairo (graphics) │ -│ ├─ jansson (JSON) │ -│ ├─ libcurl (HTTP) │ -│ └─ jansson (JSON parsing) │ -├─────────────────────────────────────────────────────────────┤ -│ 2. Module Compilation (src/ → build/) │ -│ ├─ device/config.o │ -│ ├─ srv/cc_main.o, srv/cc_conf.o, srv/cc_sensor.o │ -│ └─ mods/display.o, mods/dual.o, mods/circle.o │ -├─────────────────────────────────────────────────────────────┤ -│ 3. Linking (main.c + modules → bin/coolerdash) │ -├─────────────────────────────────────────────────────────────┤ -│ 4. Installation (make install) │ -│ ├─ Binary: /usr/libexec/coolerdash/coolerdash │ -│ ├─ Config: /etc/coolercontrol/plugins/coolerdash/config.json │ -│ ├─ Plugin UI: /etc/coolercontrol/plugins/coolerdash/ui/ │ -│ └─ Manual: /usr/share/man/man1/coolerdash.1 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Dependency Management - -**Automatic Detection:** Makefile checks for missing dependencies via `pkg-config` -**Distribution Support:** Arch, Debian, Fedora, RHEL, openSUSE -**Fallback Behavior:** Build fails with clear error messages if deps missing +Dependencies: `cairo`, `jansson`, `libcurl-gnutls`, `ttf-roboto` --- ## Configuration System -### Two-Stage Configuration Loading - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Stage 1: Hardcoded Defaults (device/config.c) │ -│ ───────────────────────────────────────────────────────────── │ -│ All Config fields initialized to built-in defaults │ -│ Function: set_*_defaults(Config *config) │ -│ Example: daemon_address = "http://localhost:11987" │ -├──────────────────────────────────────────────────────────────────┤ -│ Stage 2: JSON Config Override (device/config.c) │ -│ ───────────────────────────────────────────────────────────── │ -│ Parse config.json and override defaults │ -│ Function: load_plugin_config(Config *config, const char *path) │ -│ Path: /etc/coolercontrol/plugins/coolerdash/config.json │ -├──────────────────────────────────────────────────────────────────┤ -│ Stage 3: Device API Detection (cc_conf.c) │ -│ ───────────────────────────────────────────────────────────── │ -│ Auto-detect display dimensions from CoolerControl API │ -│ Function: update_config_from_device(Config *config) │ -│ Behavior: Only updates if width/height are 0 in config │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### Configuration Structure +Three-stage loading: -**File:** `src/device/config.h` +1. **Hardcoded defaults** — `set_*_defaults()` in `config.c` +2. **JSON override** — `load_plugin_config()` from `/etc/coolercontrol/plugins/coolerdash/config.json` +3. **API detection** — `update_config_from_device()` sets width/height if 0 -```c -typedef struct { - // Daemon connection - char daemon_address[256]; // CoolerControl API URL - char access_token[64]; // Bearer token (cc_) - - // File paths - char paths_images[PATH_MAX]; // Shutdown image directory - char paths_image_coolerdash[PATH_MAX]; // Rendered image path - char paths_image_shutdown[PATH_MAX]; // Shutdown image path - char paths_pid[PATH_MAX]; // PID file location - - // Display settings - uint16_t display_width; // LCD width in pixels (auto-detected) - uint16_t display_height; // LCD height in pixels (auto-detected) - float display_refresh_interval; // Update interval in seconds (e.g., 2.50 = 2.5 seconds) - int lcd_brightness; // 0-100 - int lcd_orientation; // 0=0°, 90=90°, 180=180°, 270=270° - char display_mode[16]; // Display mode: "dual" or "circle" - char display_shape[16]; // Shape override: "auto", "rectangular", "circular" - uint16_t circle_switch_interval; // Circle mode sensor switch interval (1-60s, default: 5) - float display_content_scale_factor; // Content scale factor (0.5-1.0, default: 0.98) +### Adding a Config Option - // Layout configuration - int layout_bar_height; // Temperature bar height - int layout_label_size; // Label font size - int layout_value_size; // Value font size - float layout_content_y_offset; // Vertical positioning (-1.0 to 1.0) - - // Font settings - char font_face[128]; // Font family name - int font_weight_label; // CAIRO_FONT_WEIGHT_* - int font_weight_value; // CAIRO_FONT_WEIGHT_* - - // Temperature thresholds - float temp_cpu_low; // CPU low threshold (°C) - float temp_cpu_medium; // CPU medium threshold (°C) - float temp_cpu_high; // CPU high threshold (°C) - float temp_gpu_low; // GPU low threshold (°C) - float temp_gpu_medium; // GPU medium threshold (°C) - float temp_gpu_high; // GPU high threshold (°C) - - // Color definitions (15 colors) - Color color_background; // Display background - Color color_cpu_label; // "CPU" text - Color color_cpu_value; // Temperature value - Color color_cpu_bar_low; // Bar color medium - // ... same for GPU - Color color_bar_background; // Bar background - Color color_bar_border; // Bar border -} Config; -``` - -### Change Tracking System - -**Purpose:** Log active configuration to systemd journal at startup - -**Implementation (config.c):** - -```c -// Called after JSON loading completes -void log_config(const Config *config); // Uses LOG_STATUS level (always visible) -``` - -**Example Output (always shown in systemd journal):** - -``` -[CoolerDash STATUS] Config loaded: mode=dual, interval=2.5s, brightness=80 -[CoolerDash STATUS] Display: 240x240, shape=auto -[CoolerDash STATUS] Daemon: http://localhost:11987 -``` - -**Note:** Changed from LOG_INFO to LOG_STATUS in version 1.96+ to ensure manual configuration changes are always visible in systemd journal, even without --verbose flag. - ---- - -## API Integration - -### CoolerControl REST API - -**Base URL:** `http://localhost:11987` (configurable) -**Authentication:** Bearer token via `Authorization: Bearer cc_` -**Session Management:** Single initialized CURL session with shared auth header - -### API Endpoints Used - -#### 1. Session Setup - -CoolerDash initializes one CURL session and stores the Bearer header built from the configured access token. - -**Implementation:** `src/srv/cc_main.c` → `init_coolercontrol_session()` - ---- - -#### 2. Device Information - -```http -GET /devices -Accept: application/json - -Response JSON: -{ - "devices": [ - { - "uid": "liquidctl-nzxt-kraken-z-0", - "name": "NZXT Kraken Z73", - "type": "Liquidctl", - "info": { - "channels": { - "lcd": { - "lcd_info": { - "screen_width": 320, - "screen_height": 320 - } - } - } - } - } - ] -} -``` - -**Implementation:** `src/srv/cc_conf.c` → `initialize_device_cache()` -**Caching:** One-time fetch at startup, stored in static struct -**Display Detection:** Kraken devices: >240x240 = circular, ≤240 = rectangular +1. Add field to `Config` struct in `config.h` +2. Set default in `set_*_defaults()` in `config.c` +3. Add JSON parsing in `load_*_from_json()` in `config.c` +4. Add to `config.json` +5. Update `docs/config-guide.md` --- -#### 3. Temperature Data - -```http -POST /status -Content-Type: application/json -Accept: application/json - -Request Body: -{ - "all": false, - "since": "1970-01-01T00:00:00.000Z" -} +## Rendering Pipeline -Response JSON: -{ - "devices": [ - { - "type": "CPU", - "status_history": [ - { - "temps": [ - { - "name": "temp1", - "temp": 45.0 - } - ] - } - ] - }, - { - "type": "GPU", - "status_history": [ - { - "temps": [ - { - "name": "GPU Core", - "temp": 52.0 - } - ] - } - ] - } - ] -} ``` - -**Implementation:** `src/srv/cc_sensor.c` → `get_temperature_data()` -**Sensor Selection:** CPU = "temp1", GPU = name contains "GPU"/"gpu" -**Validation:** Temperature range -50°C to +150°C - ---- - -#### 4. LCD Image Upload - -```http -PUT /devices/{device_uid}/settings/lcd/lcd/images?log=false -Content-Type: multipart/form-data - -Form Fields: - - mode: "image" - - brightness: "80" - - orientation: "0" - - images[]: - -Response: 200 OK +draw_display_image(config) + ├─ get_cached_lcd_device_data() + ├─ get_temperature_monitor_data() + └─ dispatch → draw_dual_image() or draw_circle_image() + ├─ cairo_image_surface_create(ARGB32, w, h) + ├─ cairo_create(surface) + ├─ draw background + bars + labels + temperatures + ├─ cairo_surface_write_to_png(surface, path) + ├─ cairo_destroy(cr) + cairo_surface_destroy(surface) + └─ send_image_to_lcd(config, path, uid) ``` -**Implementation:** `src/srv/cc_main.c` → `send_image_to_lcd()` -**Image Format:** PNG, dimensions match device LCD -**Multipart Construction:** CURL mime API with proper MIME types +### Display Shape ---- - -### HTTP Session Management - -**Session Lifecycle:** +- NZXT Kraken ≤240×240 → rectangular (inscribe_factor = 1.0) +- NZXT Kraken >240×240 → circular (inscribe_factor = 1/√2 ≈ 0.7071) +- Override via `config.json`: `"shape": "rectangular"` or `"circular"` -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. init_coolercontrol_session() │ -│ ├─ curl_global_init() │ -│ ├─ curl_easy_init() │ -│ ├─ Build Authorization: Bearer cc_ header │ -│ └─ Set session_initialized = 1 │ -├─────────────────────────────────────────────────────────────┤ -│ 2. Repeated API Calls (reuse initialized session) │ -│ ├─ send_image_to_lcd() - every 60s │ -│ └─ get_temperature_data() - every 60s │ -├─────────────────────────────────────────────────────────────┤ -│ 3. cleanup_coolercontrol_session() │ -│ ├─ curl_easy_cleanup() │ -│ ├─ curl_global_cleanup() │ -│ └─ Set session_initialized = 0 │ -└─────────────────────────────────────────────────────────────┘ -``` +### Scaling -**Security Features:** -- Explicit Bearer auth header on each API request -- Cleanup protection with static flag (prevents double-free) +Base resolution: 240×240. Content scales dynamically. +Circular displays: `safe_area = display_width × inscribe_factor` --- -## Rendering Pipeline +## Code Style -### Cairo Graphics Workflow - -**Library:** cairo 1.x (vector graphics) -**Output Format:** PNG image matching LCD dimensions -**Color Depth:** 24-bit RGB - -### Rendering Steps - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ 1. draw_display_image(config) │ -│ ├─ Get device info from cache │ -│ ├─ Fetch temperature data (cc_sensor.c) │ -│ └─ Dispatch to draw_dual_image() or draw_circle_image() │ -├──────────────────────────────────────────────────────────────────┤ -│ 2. render_display(config, data, device_name) │ -│ ├─ Detect display shape (circular vs rectangular) │ -│ ├─ Calculate scaling parameters │ -│ │ ├─ Circular: inscribe_factor = 1/√2 (≈0.7071, default) -│ │ │ * Can be overridden in `config.json` with `inscribe_factor` (>=0; 0 = auto) -│ │ └─ Rectangular: inscribe_factor = 1.0 │ -│ ├─ Create Cairo surface (width × height × ARGB32) │ -│ ├─ Draw background │ -│ ├─ Draw CPU section (label + value + bar) │ -│ ├─ Draw GPU section (label + value + bar) │ -│ ├─ Write PNG to /etc/coolercontrol/plugins/coolerdash/coolerdash.png │ -│ └─ Upload to LCD via send_image_to_lcd() │ -└──────────────────────────────────────────────────────────────────┘ -``` +- C99 + POSIX.1-2001 +- 4 spaces, no tabs, 120 char max line +- Functions: `snake_case`, Structs: `PascalCase`, Constants: `UPPER_SNAKE_CASE` +- Use `cc_safe_strcpy()` instead of `strcpy`/`strncpy` +- Check every `malloc`, `fopen`, `curl_easy_perform` +- Return 1 on success, 0 on failure -### Display Shape Detection - -**File:** `src/srv/cc_conf.c` → `is_circular_display_device()` - -**Algorithm:** +### Logging ```c -int is_circular_display_device(const char *device_name, int width, int height) { - const int is_kraken = (strstr(device_name, "Kraken") != NULL); - - if (is_kraken) { - // NZXT Kraken: >240x240 = circular, ≤240 = rectangular - const int is_large_display = (width > 240 || height > 240); - return is_large_display ? 1 : 0; - } - - // Add other circular display brands here - return 0; -} +log_message(LOG_INFO, "..."); // --verbose only +log_message(LOG_STATUS, "..."); // always shown +log_message(LOG_WARNING, "..."); // always shown +log_message(LOG_ERROR, "..."); // always shown ``` -### Scaling unit tests -There is a small test harness at `tests/test_scaling.c` that validates `safe_area` and `safe_bar` calculations. -Use it to verify behavior across cases: +--- -- `inscribe_factor = 0.0` → auto fallback (1/√2) -- `inscribe_factor = 0.70710678` → explicit geometry -- `inscribe_factor = 0.85` → custom factor -- For rectangular overrides the `inscribe_factor = 1.0`. -- Invalid values (e.g. -1 or 1.5) fallback to the geometric factor. +## Testing -Build & run: ```bash +# Unit test gcc -std=c99 -Iinclude -I./src -o build/test_scaling tests/test_scaling.c -lm ./build/test_scaling -``` - -This script prints a table summarizing `inscribe_used`, `safe_area`, and `safe_bar` for the test cases and whether they meet expectations. - - -**Known Devices:** -- **NZXT Kraken Z53** (280x280) → Circular -- **NZXT Kraken Z73** (320x320) → Circular -- **NZXT Kraken Elite** (640x640) → Circular -- **NZXT Kraken (240x240 or smaller)** → Rectangular - -### Circular Display Rendering - -**Challenge:** Circular LCD requires content within inscribed square -**Solution:** Apply inscribe factor 1/√2 ≈ 0.7071 to all coordinates - -**Mathematical Basis:** - -``` -Circle radius = R -Inscribed square side = R * √2 / 2 = R * 1/√2 -``` - -**Example (320x320 circular display):** - -``` -Original coordinates: (0, 0) to (320, 320) -Safe drawing area: (48, 48) to (272, 272) [≈70.71% of original] -``` - -**Implementation (dual.c):** - -```c -ScalingParams params; - -// Priority: shape config > force flag > auto-detection -if (strcmp(config->display_shape, "rectangular") == 0) { - params.inscribe_factor = 1.0; -} else if (strcmp(config->display_shape, "circular") == 0) { - params.inscribe_factor = M_SQRT1_2; -} else { - // Auto-detection fallback - int is_circular = config->force_display_circular || - is_circular_display_device(device_name, width, height); - params.inscribe_factor = is_circular ? M_SQRT1_2 : 1.0; -} - -params.safe_content_margin = (1.0 - params.inscribe_factor) / 2.0; // ≈0.1464 - -// Apply to all drawing coordinates -double safe_x = params.safe_content_margin * config->display_width; -double safe_y = params.safe_content_margin * config->display_height; -``` - -**Configuration Override (v1.96+):** - -```ini -[display] -# Manual override (recommended for testing/troubleshooting) -shape = auto # or "rectangular" or "circular" -``` - -### Temperature Bar Rendering - -**Color Selection Logic:** - -```c -const Color *get_bar_color(float temp, float low, float medium) { - if (temp < low) - return &config->color_cpu_bar_low; // Green - else if (temp < medium) - return &config->color_cpu_bar_medium; // Yellow - else - return &config->color_cpu_bar_high; // Red -} -``` - -**Fill Width Calculation:** - -```c -int calculate_temp_fill_width(float temp, int max_width, float max_temp) { - if (temp <= 0.0f) - return 0; - - const float ratio = fminf(temp / max_temp, 1.0f); // Clamp to 1.0 - return (int)(ratio * max_width); -} -``` - ---- - -## Function Reference - -### Module: main.c (Daemon Lifecycle) - -#### Core Functions - -| Function | Purpose | Returns | -|----------|---------|---------| -| `main(argc, argv)` | Entry point, orchestrates daemon lifecycle | `int` exit code | -| `parse_arguments()` | Parse CLI args (`--verbose`, `-v`, `--help`, etc.) | `const char*` config path | -| `initialize_config_and_instance()` | Load config, check existing instance | `int` success | -| `initialize_coolercontrol_services()` | Start API session, cache devices | `int` success | -| `run_daemon(config)` | Main loop: read temps → render → sleep 60s | `int` exit code | - -#### Signal Handling - -| Function | Purpose | -|----------|---------| -| `setup_enhanced_signal_handlers()` | Register SIGTERM/SIGINT/SIGHUP handlers | -| `handle_shutdown_signal(signum)` | Display shutdown image, stop loop | - -#### PID Management - -| Function | Purpose | -|----------|---------| -| `write_pid_file(path)` | Atomic PID file creation with O_EXCL | -| `remove_pid_file(path)` | Secure deletion with validation | -| `check_existing_instance_and_handle(path)` | Detect running instance via PID | - -#### Cleanup - -| Function | Purpose | -|----------|---------| -| `perform_cleanup(config)` | Cleanup session + PID + temp image | -| `send_shutdown_image_if_needed()` | Upload shutdown.png on exit | - ---- - -### Module: device/config.c (Configuration System) - -| Function | Purpose | -|----------|---------| -| `load_plugin_config(config, path)` | Load JSON config, apply defaults, log result | -| `set_*_defaults(config)` | Initialize subsystem fields with hardcoded defaults | -| `log_message(level, format, ...)` | Global logging function (respects verbose flag) | - -**Default Values:** - -```c -daemon_address = "http://localhost:11987" -access_token = "" -display_width = 0 // auto-detected from API -display_refresh_interval = 2.5 -lcd_brightness = 80 -``` - -**JSON Sections:** - -```json -{ - "daemon": { "address": "...", "access_token": "cc_" }, - "display": { "width": 0, "height": 0, "brightness": 80, "mode": "dual" }, - "layout": { "bar_height": 30, "label_size": 18, "value_size": 24 }, - "font": { "face": "Roboto" }, - "temperature": { "cpu_low": 50, "cpu_medium": 70, "cpu_high": 85 }, - "colors": { "background": [0,0,0], "cpu_label": [255,255,255] } -} - ---- - -### Module: srv/cc_main.c (Session Management) - -#### Public API (4 functions) - -| Function | Purpose | Returns | -|----------|---------|---------| -| `init_coolercontrol_session(config)` | Initialize CURL session and Bearer header | `int` success | -| `is_session_initialized()` | Check session state | `int` boolean | -| `cleanup_coolercontrol_session()` | Cleanup CURL session resources | `void` | -| `send_image_to_lcd(config, image_path, device_uid)` | Upload PNG to LCD | `int` success | - -#### Internal Helpers (15 functions) - -```c -cc_init_response_buffer() // Allocate HTTP response buffer -write_callback() // libcurl write callback -build_login_credentials() // Construct login URL + credentials -configure_login_curl() // Setup CURL for login -build_lcd_upload_form() // Create multipart form (mode/brightness/image) -add_mime_field() // Add string field to form -add_image_file_field() // Add PNG file to form -configure_lcd_upload_curl() // Setup CURL for upload -validate_upload_params() // Check parameters before upload -check_upload_response() // Validate HTTP 200 response -``` - -**Session State:** - -```c -static CoolerControlSession cc_session = { - .curl_handle = NULL, - .cookie_jar = {0}, - .session_initialized = 0 -}; -``` - ---- - -### Module: srv/cc_conf.c (Device Cache) - -#### Public API (4 functions) - -| Function | Purpose | Returns | -|----------|---------|---------| -| `init_device_cache(config)` | Fetch device info once (GET /devices) | `int` success | -| `get_cached_lcd_device_data(config, uid, name, width, height)` | Read cached data | `int` success | -| `update_config_from_device(config)` | Auto-set width/height if 0 | `int` updated | -| `is_circular_display_device(name, width, height)` | Detect display shape | `int` boolean | - -#### Internal Helpers (16 functions) - -```c -initialize_device_cache() // HTTP GET + JSON parse + cache population -parse_lcd_device_data() // Extract device info from JSON -extract_device_type_from_json() // Get "type" field -extract_device_uid() // Get "uid" field -extract_device_name() // Get "name" field -extract_lcd_dimensions() // Get screen_width/screen_height -search_lcd_device() // Find first CoolerControl LCD device in array -configure_device_cache_curl() // Setup CURL for /devices request -process_device_cache_response() // Parse response + populate cache -``` - -**Cache Structure:** - -```c -static struct { - int initialized; - char device_uid[128]; - char device_name[CC_NAME_SIZE]; - int screen_width; - int screen_height; -} device_cache = {0}; -``` - ---- - -### Module: srv/cc_sensor.c (Temperature Monitoring) - -#### Public API (1 function) - -| Function | Purpose | Returns | -|----------|---------|---------| -| `get_temperature_monitor_data(config, data)` | Read CPU/GPU temps | `int` success | - -**Data Structure:** - -```c -typedef struct { - float temp_cpu; - float temp_gpu; -} monitor_sensor_data_t; -``` - -#### Internal Helpers (5 functions) - -```c -get_temperature_data() // HTTP POST /status + parse response -parse_temperature_data() // Extract temps from JSON -extract_device_temperature() // Get temp from status_history[last].temps[] -configure_status_request() // Setup CURL for /status POST -``` - -**Temperature Validation:** - -```c -if (temperature < -50.0f || temperature > 150.0f) - continue; // Invalid, skip sensor -``` - ---- - -### Module: mods/display.c + dual.c + circle.c (Rendering) - -#### Public API - -| Function | Purpose | Returns | -|----------|---------|---------| -| `draw_display_image(config)` | Dispatch to dual or circle mode | `void` | -| `draw_dual_image(config)` | Render CPU+GPU simultaneously, upload | `void` | -| `draw_circle_image(config)` | Render alternating sensor slot, upload | `void` | - -#### Internal Helpers - -```c -calculate_scaling_params() // Compute inscribe factor, margins -draw_background() // Fill background color -draw_temperature_section() // Render CPU/GPU label+value+bar -draw_temperature_label() // Render "CPU"/"GPU" text -draw_temperature_value() // Render "45.0°C" text -draw_temperature_bar() // Render colored bar with border -get_bar_color() // Select color based on thresholds -calculate_temp_fill_width() // Compute bar fill width from temp -set_cairo_color() // Convert Color struct to cairo RGB -cairo_color_convert() // uint8_t (0-255) → double (0.0-1.0) -``` - -**Rendering Workflow:** - -```c -cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); -cairo_t *cr = cairo_create(surface); - -// Draw background -cairo_set_source_rgb(cr, bg_r, bg_g, bg_b); -cairo_paint(cr); - -// Draw CPU section (top half) -draw_temperature_section(cr, config, params, data->temp_cpu, "CPU", CPU); - -// Draw GPU section (bottom half) -draw_temperature_section(cr, config, params, data->temp_gpu, "GPU", GPU); - -// Save to PNG -cairo_surface_write_to_png(surface, "/etc/coolercontrol/plugins/coolerdash/coolerdash.png"); -cairo_destroy(cr); -cairo_surface_destroy(surface); -``` - ---- - -## Development Guidelines - -### Code Style - -- **Standard:** C99 with POSIX.1-2001 extensions -- **Indentation:** 4 spaces (no tabs) -- **Line Length:** 120 characters maximum -- **Naming:** - - Functions: `snake_case` - - Structs: `PascalCase` (e.g., `Config`, `ScalingParams`) - - Constants: `UPPER_SNAKE_CASE` - - Static functions: `static` prefix, file-scope only - -### Error Handling - -**Pattern:** Check every allocation, API call, file operation - -```c -// Example: Safe malloc -char *buffer = malloc(size); -if (!buffer) { - log_message(LOG_ERROR, "Memory allocation failed"); - return 0; -} - -// Example: CURL error checking -CURLcode res = curl_easy_perform(curl); -if (res != CURLE_OK) { - log_message(LOG_ERROR, "CURL failed: %s", curl_easy_strerror(res)); - return 0; -} - -// Example: File operation -FILE *fp = fopen(path, "r"); -if (!fp) { - log_message(LOG_ERROR, "Cannot open file: %s", path); - return 0; -} -``` -### Memory Safety +# Debug build +make clean && make debug -**Rules:** - -1. **Always free what you allocate** (RAII pattern) -2. **Check for NULL before dereferencing** -3. **Use fixed-size buffers with bounds checking** -4. **Prefer stack allocation for small objects** - -**Example (cleanup pattern):** - -```c -int my_function(void) { - char *buffer = malloc(1024); - if (!buffer) - return 0; - - CURL *curl = curl_easy_init(); - if (!curl) { - free(buffer); - return 0; - } - - // ... use resources ... - - // Cleanup (always reached) - curl_easy_cleanup(curl); - free(buffer); - return 1; -} -``` - -### Logging Levels - -```c -typedef enum { - LOG_INFO, // Only shown with --verbose flag - LOG_STATUS, // Always shown (important state changes) - LOG_WARNING, // Always shown (non-fatal issues) - LOG_ERROR // Always shown (fatal errors) -} log_level_t; -``` - -**Usage:** - -```c -log_message(LOG_INFO, "Reading config from %s", path); // Verbose only -log_message(LOG_STATUS, "Session initialized successfully"); // Always -log_message(LOG_WARNING, "Using fallback value for %s", key); // Always -log_message(LOG_ERROR, "Failed to connect to API"); // Always -``` - -### Adding New Configuration Options - -**Steps:** - -1. **Add field to Config struct** (`src/device/config.h`) -2. **Add default value** (`src/device/config.c` → appropriate `set_*_defaults()`) -3. **Add JSON parsing** (`src/device/config.c` → `load_*_from_json()` function) -4. **Update documentation** (`docs/config-guide.md`) -5. **Update example config** (`/etc/coolercontrol/plugins/coolerdash/config.json`) - -**Example 1: Adding Circle Mode Switch Interval (uint16_t with validation)** - -```c -// 1. config.h - Add field to Config struct -typedef struct { - // ... - uint16_t circle_switch_interval; // Circle mode sensor switch interval (1-60s) -} Config; - -// 2. config.c - Set default value -static void set_display_defaults(Config *config) { - if (config->circle_switch_interval == 0) - config->circle_switch_interval = 5; -} - -// 3. config.c - Add JSON parsing -static void load_display_from_json(Config *config, json_t *display) { - json_t *val = json_object_get(display, "circle_switch_interval"); - if (json_is_integer(val)) { - long v = json_integer_value(val); - if (v >= 1 && v <= 60) - config->circle_switch_interval = (uint16_t)v; - else - log_message(LOG_WARNING, "circle_switch_interval must be 1-60, using default: 5"); - } -} -``` - -**Example 2: Adding Content Scale Factor (float with validation)** - -```c -// 1. config.h - Add field to Config struct -typedef struct { - // ... - float display_content_scale_factor; // Content scale factor (0.5-1.0) -} Config; - -// 2. config.c - Set default value -static void set_display_defaults(Config *config) { - // ... - config->display_content_scale_factor = 0.98f; // Default: 98% (2% margin) -} - -// 3. config.c - Add JSON parsing -static void load_display_from_json(Config *config, json_t *display) { - json_t *val = json_object_get(display, "content_scale_factor"); - if (json_is_real(val)) { - float f = (float)json_real_value(val); - if (f >= 0.5f && f <= 1.0f) - config->display_content_scale_factor = f; - } -} -``` - -**Example 3: Adding New Color** - -```c -// 1. config.h -typedef struct { - // ... - Color color_my_new_element; -} Config; - -// 2. config.c - Set default value -static void set_color_defaults(Config *config) { - // ... - config->color_my_new_element = (Color){0, 255, 0}; // Green -} - -// 3. config.c - Add JSON parsing -static void load_colors_from_json(Config *config, json_t *colors) { - json_t *my_obj = json_object_get(colors, "my_new_element"); - if (json_is_object(my_obj)) - parse_color_from_json(my_obj, &config->color_my_new_element); -} -``` - -**Configuration Best Practices:** -- Always provide sensible defaults in `set_*_defaults()` -- Validate user input with clear error messages using `log_message(LOG_WARNING, ...)` -- Use appropriate data types (`uint16_t` for small integers, `float` for decimals) -- Document acceptable ranges in both code comments and `config.json` -- Log warnings for invalid values and fallback to defaults - ---- - -## Testing & Debugging - -### Manual Testing - -```bash -# 1. Stop service -sudo systemctl stop coolerdash.service - -# 2. Run manually with verbose logging +# Manual run +sudo systemctl stop coolercontrold coolerdash --verbose -# 3. Watch for errors/warnings in output -# Expected: [CoolerDash STATUS] messages every 60s - -# 4. Check generated image -ls -lh /tmp/coolerdash.png -file /tmp/coolerdash.png # Should show: PNG image data, 320 x 320, ... -``` - -### Debug Build - -```bash -# Compile with debug symbols + AddressSanitizer -make clean -make debug - -# Run under debugger -gdb bin/coolerdash -(gdb) run --verbose - -# Check for memory leaks -# AddressSanitizer will report any leaks on exit -``` - -### Service Debugging - -```bash -# Check service status -systemctl status coolerdash.service - -# View recent logs -journalctl -u coolerdash.service -n 50 - -# Follow logs in real-time +# Service logs journalctl -xeu coolerdash.service -f - -# Restart service -sudo systemctl restart coolerdash.service -``` - -### Common Issues - -#### Issue: "Session initialization failed" - -**Cause:** CoolerControl not running or wrong credentials -**Debug:** - -```bash -# Check CoolerControl status -systemctl status coolercontrold - -# Check configured access token -grep access_token /etc/coolercontrol/plugins/coolerdash/config.json -``` - ---- - -#### Issue: "No LCD devices found" - -**Cause:** Device not detected or wrong type -**Debug:** - -```bash -# List devices via API -curl http://localhost:11987/devices | jq '.devices[] | {uid, name, type}' - -# Check for Liquidctl type -# Expected: "type": "Liquidctl" -``` - ---- - -#### Issue: "Display wrong shape (circular/rectangular)" - -**Cause:** Incorrect display detection logic -**Debug:** - -```bash -# Run with verbose logging -coolerdash --verbose - -# Look for log message: -# [CoolerDash STATUS] Display shape: circular (based on 320x320 Kraken) -# or -# [CoolerDash STATUS] Display shape: rectangular (based on 240x240) - -# Force circular mode (developer flag) -coolerdash --develop # All displays treated as circular -``` - ---- - -#### Issue: "Temperature values incorrect" - -**Cause:** Wrong sensor selection -**Debug:** - -```bash -# Check raw API response -curl -u CCAdmin:coolAdmin -X POST http://localhost:11987/status \ - -H "Content-Type: application/json" \ - -d '{"all": false, "since": "1970-01-01T00:00:00.000Z"}' | jq - -# Look for temps[] arrays in CPU/GPU devices -# Verify sensor names match expected patterns: -# CPU: "temp1" -# GPU: contains "GPU" or "gpu" -``` - ---- - -### Profiling - -**CPU Usage Monitoring:** - -```bash -# Check daemon resource usage -ps aux | grep coolerdash - -# Expected: ~0.1% CPU (idle most of time, 60s sleep) -# Memory: ~10-20 MB RSS -``` - -**API Call Timing:** - -```bash -# Enable verbose logging -coolerdash --verbose 2>&1 | grep -E "(Session|Temperature|Upload)" - -# Expected timing: -# Session init: <1s -# Temperature fetch: <500ms -# Image render: <200ms -# LCD upload: <1s ``` --- -## Performance Characteristics - -### Resource Usage - -- **Memory:** ~15 MB RSS (static allocations + Cairo surface) -- **CPU (idle):** <0.1% (60s sleep between updates) -- **CPU (active):** ~5-10% (during rendering + upload, <2s every 60s) -- **Network:** ~50 KB/min (API calls + image uploads) -- **Disk I/O:** Minimal (one PNG write per update) - -### Optimization Strategies - -1. **Device Caching:** One-time API call instead of per-frame lookup -2. **Exponential Buffer Growth:** Reduce realloc() calls for HTTP responses -3. **Static Session:** Persistent CURL handle + cookie reuse -4. **Stack Allocation:** Small structs on stack (ScalingParams, etc.) -5. **Efficient Scaling Math:** Pre-computed inscribe factors - ---- - -## Future Development - -### Potential Enhancements - -- [ ] **Multi-display support:** Handle multiple LCD devices simultaneously -- [ ] **Plugin system:** External modules for custom visualizations -- [ ] **GPU utilization:** Show usage % in addition to temperature -- [ ] **Network stats:** Bandwidth monitoring on display -- [ ] **Custom images:** User-provided background/overlay images -- [ ] **Animation support:** GIF/APNG rendering -- [ ] **Themes:** Predefined color/layout presets -- [ ] **Web UI:** Browser-based configuration editor - -### Contributing - -**Repository:** https://github.com/damachine/coolerdash -**Issues:** https://github.com/damachine/coolerdash/issues -**Discussions:** https://github.com/damachine/coolerdash/discussions - -**Pull Request Checklist:** - -- [ ] Code follows C99 standard + style guidelines -- [ ] No compiler warnings with `-Wall -Wextra` -- [ ] All allocations have matching frees -- [ ] Error paths properly handled -- [ ] Tested with `make debug` (AddressSanitizer) -- [ ] Documentation updated (if adding features) -- [ ] CHANGELOG.md entry added - ---- - -## License - -**MIT License** - See LICENSE file for full text. - -**Copyright (c) 2025 damachine (damachin3@proton.me)** - ---- - ## References -### External Documentation - -- **CoolerControl API:** https://gitlab.com/coolercontrol/coolercontrol/-/blob/main/openapi-spec.json -- **Cairo Graphics:** https://www.cairographics.org/manual/ -- **libcurl:** https://curl.se/libcurl/c/ -- **Jansson JSON:** https://jansson.readthedocs.io/ - -### Project Files - -- **Configuration Guide:** `docs/config-guide.md` -- **Supported Devices:** `docs/devices.md` -- **Display Detection:** `docs/display-detection.md` -- **Example Config:** `etc/coolercontrol/plugins/coolerdash/config.json` - ---- - -**Document Version:** 2.x -**Last Updated:** 2026 -**Maintained by:** damachine (damachin3@proton.me) +- CoolerControl API: https://gitlab.com/coolercontrol/coolercontrol +- Cairo: https://www.cairographics.org/manual/ +- libcurl: https://curl.se/libcurl/c/ +- Jansson: https://jansson.readthedocs.io/ diff --git a/docs/devices.md b/docs/devices.md index faf8339..f3862aa 100644 --- a/docs/devices.md +++ b/docs/devices.md @@ -1,25 +1,20 @@ -## Supported Devices (Compatibility List) +## Supported Devices -### This file contains a community-maintained list of devices confirmed to work with **Coolerdash**. -### If Coolerdash works or not on your device, please submit a [Device Confirmation Issue](https://github.com/damachine/coolerdash/issues/new?template=device-confirmation.yml). ---- +If CoolerDash works (or not) on your device, submit a [Device Confirmation Issue](https://github.com/damachine/coolerdash/issues/new?template=device-confirmation.yml). ## Confirmed Devices -| Manufacturer | Model / Device | Status | Tester (GitHub) | Date | -|------------------|--------------------------|---------------|-------------------------|------------| -| NZXT | Kraken 2023 | ✅ Working | @damachine | 2025-06-08 | -| NZXT | Kraken 2023 | ✅ Working | @Kimloc (discord) | 2025-08-27 | -| NZXT | Kraken 2023 | ✅ Working | @olivetti80 | 2025-09-12 | -| NZXT | Kraken 2023 Elite | ⚠️ Partially | @Mondkeks | 2025-10-09 | -| NZXT | Kraken Z63 | ✅ Working | @SSUPD-Beast | 2025-11-24 | -| NZXT | Kraken Plus 240 | ❌ Not working | @K-Michallik | 2026-02-23 | -| ? | ? | ? | ? | ? | -|------------------|--------------------------|---------------|-------------------------|------------| - ---- +| Manufacturer | Model | Status | Tester | Date | +|-------------|-------|--------|--------|------| +| NZXT | Kraken 2023 | ✅ Working | @damachine | 2025-06-08 | +| NZXT | Kraken 2023 | ✅ Working | @Kimloc (discord) | 2025-08-27 | +| NZXT | Kraken 2023 | ✅ Working | @olivetti80 | 2025-09-12 | +| NZXT | Kraken 2023 Elite | ⚠️ Partially | @Mondkeks | 2025-10-09 | +| NZXT | Kraken Z63 | ✅ Working | @SSUPD-Beast | 2025-11-24 | +| NZXT | Kraken Plus 240 | ❌ Not working | @K-Michallik | 2026-02-23 | ### Legend -- ✅ Working -- ⚠️ Partially working (with limitations) + +- ✅ Working +- ⚠️ Partially working - ❌ Not working diff --git a/docs/display-detection.md b/docs/display-detection.md index 1ecf751..034aa4b 100644 --- a/docs/display-detection.md +++ b/docs/display-detection.md @@ -1,364 +1,77 @@ +# Display Shape Detection -# Display Shape Detection System +Determines whether a display is circular or rectangular. Affects inscribe factor and layout calculations. -## Overview - -The system automatically detects whether a display is **circular** (round) or **rectangular** and adjusts calculations accordingly. You can also **manually override** the detection using the `shape` configuration parameter. - -## Configuration Override - -**Manual control via `config.json`** +## Config Override ```json -{ - "display": { +"display": { "shape": "auto" - } } ``` -> Values: `"auto"` (default), `"rectangular"`, `"circular"` - -### Priority System - -1. **`shape` config parameter** (highest priority - manual override) -2. **Automatic detection** (default behavior) - -**Examples:** -```bash -# Config file controls shape -coolerdash # Uses shape from config.json -``` +Values: `"auto"` (default), `"rectangular"`, `"circular"` -## Automatic Detection +Priority: `shape` config > auto-detection. -### 1. NZXT Kraken Device Logic (Resolution-Based) +## Auto-Detection -**Special handling for NZXT Kraken devices - resolution determines shape:** +### NZXT Kraken (resolution-based) ```c -// NZXT Kraken display shape rules: -// - 240x240 or smaller = RECTANGULAR (no inscribe factor) -// - Larger than 240x240 (e.g., 320x320) = CIRCULAR (with inscribe factor) - -if (is_kraken) { - const int is_large_display = (screen_width > 240 || screen_height > 240); - return is_large_display ? 1 : 0; // 1 = circular, 0 = rectangular -} +// ≤240×240 → rectangular (inscribe_factor = 1.0) +// >240×240 → circular (inscribe_factor = 0.7071) ``` -**Examples:** -- NZXT Kraken 2023 (240×240) → **Rectangular** (inscribe factor = 1.0) -- NZXT Kraken Z (320×320) → **Circular** (inscribe factor = 0.7071) +### Other Devices -### 2. Device Database for Non-Kraken Devices +Checked against `circular_devices[]` database in `cc_conf.c`. Unknown devices default to rectangular. -For other brands, the function `is_circular_display_device()` uses a database: - -```c -// Database of non-Kraken devices with CIRCULAR displays -const char *circular_devices[] = { - // Add other brands with circular displays here - // Example: "Corsair LCD Circular Model" -}; -``` +## Inscribe Factor (1/√2) -### 3. Automatic Detection Priority (when shape=auto) - -1. **NZXT Kraken devices**: Resolution-based detection (≤240×240 = rectangular, >240×240 = circular) -2. **Other devices**: Check device name database -3. **Default**: Rectangular (safer fallback) - -**Note:** Manual override via `shape` config parameter takes precedence over all automatic detection methods. - -## Math Explanation - -### Why 0.7071 (1/√2)? - -**Problem**: Prevent clipping on circular displays +Circular displays need content within an inscribed square to prevent clipping. ``` -Circular Display (Radius = R) -┌─────────────────────┐ -│ ╱───╲ │ -│ ╱ ╲ │ -│ │ │ │ ← Circular viewport -│ │ [TEXT] │ │ ← Rectangular content must fit inside -│ ╲ ╱ │ -│ ╰───╯ │ -└─────────────────────┘ +Circle radius = R +Inscribed square side = R × √2 +Ratio = 1/√2 ≈ 0.7071 ``` -**Solution**: Largest square that fits in a circle +Configurable via `inscribe_factor` in `config.json` (default: 0.70710678, `0` = auto). -``` -If circle radius = R: -- Diameter = 2R -- Inscribed square diagonal = 2R -- Inscribed square side = 2R / √2 = R√2 -- Safe area ratio = (R√2) / (2R) = √2/2 = 1/√2 ≈ 0.7071 -``` +### Calculation -**Example** (240×240 circular display): -- Without factor: Bars would be 240px wide → **CLIPPED at edges** -- With factor: Bars are 240 × 0.7071 = 169.7px wide → **Fully visible** +| Display | Resolution | Shape | inscribe_factor | safe_area | margin | +|---------|-----------|-------|-----------------|-----------|--------| +| Kraken 2023 | 240×240 | rectangular | 1.0 | 240px | ~2px | +| Kraken Z | 320×320 | circular | 0.7071 | ~226px | ~47px | -## Calculation Examples +## Adding Devices -### NZXT Kraken 2023 (240×240 - Rectangular) - -**Circular Displays:** -``` -inscribe_factor = 0.7071 (reduced safe area) - NOTE: The `inscribe_factor` is configurable via `display_inscribe_factor` in `config.json`; default is 0.70710678 (0.0 = auto) -safe_area_width = 240 × 0.7071 = ~170px -safe_bar_width = 170 × 0.98 = ~167px -margin = (240 - 170) / 2 = ~35px -``` - -**Rectangular Displays:** -``` -inscribe_factor = 1.0 (no reduction) -safe_area_width = 240 × 1.0 = 240px -safe_bar_width = 240 × 0.98 = ~235px -margin = (240 - 235) / 2 = ~2.5px -``` +### Circular (non-Kraken) -### NZXT Kraken Z (320×320 - Circular) - -**Circular Displays:** -``` -inscribe_factor = 0.7071 (reduced safe area) -safe_area_width = 320 × 0.7071 = ~226px -safe_bar_width = 226 × 0.98 = ~221px -margin = (320 - 226) / 2 = ~47px -``` - -## Adding New Devices - -### Adding a Circular Display (Non-Kraken) - -Edit `src/srv/cc_conf.c`, function `is_circular_display_device()`: +Add to `circular_devices[]` in `src/srv/cc_conf.c`: ```c const char *circular_devices[] = { - "Corsair LCD Circular", // Example - "Your Device Name", // Add here + "Your Device Name", }; ``` -**Important:** The device name only needs to be **contained**, not exactly match! -- `"Corsair LCD"` matches: "Corsair LCD Circular 240", "Corsair LCD", etc. - -### NZXT Kraken Devices - -**No database entry needed!** Resolution-based detection: -- **240×240 or smaller**: Automatically rectangular -- **Larger than 240×240**: Automatically circular - -### Rectangular Display - -Rectangular displays do **not** need to be added to the database! - -The system treats all unknown devices as **rectangular** (safer fallback). - -## Detection Examples - -| Device Name | Resolution | Detection Result | Inscribe Factor | Reason | -|-----------------------|-----------|------------------|-----------------|----------------------------| -| NZXT Kraken 2023 | 240×240 | **Rectangular** | 1.0 | Kraken ≤240×240 | -| NZXT Kraken Z | 320×320 | **Circular** | 0.7071 | Kraken >240×240 | -| Generic Device | 240×240 | **Rectangular** | 1.0 | Not in database, default | -| Custom Circular LCD | 240×240 | **Rectangular** | 1.0 | Needs database entry | - -## Logging - -The system logs the detection: - -``` -[INFO] Circular display detected (device: NZXT Kraken Z, inscribe factor: 0.7071) -[INFO] Rectangular display detected (device: NZXT Kraken 2023, inscribe factor: 1.0000) -``` - -## Visualization - -### Circular Display (240×240) - NZXT Kraken Z +Name matching uses `strstr` — partial matches work. -``` -┌─────────────────────┐ -│ [invisible] │ -│ ╭─────────╮ │ -│ ╱ ╲ │ -│ │ CPU 33° │ │ ← safe_area_width = 170px -│ │ ████░░░░░ │ │ ← bar_width = 167px -│ │ GPU 46° │ │ -│ ╲ ╱ │ -│ ╰─────────╯ │ -│ [invisible] │ -└─────────────────────┘ - margin = 37px -``` - -### Rectangular Display (240×240) - NZXT Kraken 2023 - -``` -┌─────────────────────┐ -│ CPU 33° │ -│ ████████████████░░░ │ ← bar_width = 235px -│ │ -│ GPU 46° │ -│ ████████████████░░░ │ -│ │ -└─────────────────────┘ - margin = 2.5px -``` +### NZXT Kraken -## API Reference +No database entry needed. Resolution-based detection is automatic. -### Function: `is_circular_display_device()` +### Rectangular -**Location**: `src/srv/cc_conf.c` - -**Signature**: -```c -int is_circular_display_device(const char *device_name, int screen_width, int screen_height) -``` - -**Parameters**: -- `device_name`: Device identifier string (e.g., "NZXT Kraken 2023") -- `screen_width`: Display width in pixels -- `screen_height`: Display height in pixels - -**Returns**: -- `1` if circular display detected -- `0` if rectangular display detected - -**Logic**: -1. Check if device name contains "Kraken" -2. If Kraken: Use resolution-based detection (>240×240 = circular) -3. If not Kraken: Check device database -4. Default: Rectangular (safer fallback) - -### C Functions - -```c -// In cc_conf.h/c -int is_circular_display_device(const char *device_name, int screen_width, int screen_height); - -// In display.c -static void calculate_scaling_params( - const struct Config *config, - ScalingParams *params, - const char *device_name -); - -void draw_display_image(const struct Config *config); -``` - -## Testing - -### Manual Test - -1. Compile: - ```bash - make - ``` - -2. Install and restart service: - ```bash - makepkg -fsi - systemctl daemon-reload - systemctl restart coolerdash.service - ``` - -3. Check logs: - ```bash - journalctl -u coolerdash.service -f - ``` - -Expected output for **NZXT Kraken 2023 (240×240)**: -``` -[INFO] Rectangular display detected (device: NZXT Kraken 2023, inscribe factor: 1.0000) -[INFO] Sending image to LCD: NZXT Kraken 2023 [abc123...] -``` - -Expected output for **NZXT Kraken Z (320×320)**: -``` -[INFO] Circular display detected (device: NZXT Kraken Z, inscribe factor: 0.7071) -[INFO] Sending image to LCD: NZXT Kraken Z [abc123...] -``` +No action needed. Unknown devices default to rectangular. ## Troubleshooting -### Problem: Clipping on circular display - -**Cause**: Device not detected as circular - -**Solution Options**: -1. **Quick fix (recommended)**: Set `shape` to `"circular"` in `config.json` -2. For NZXT Kraken: Verify resolution is >240×240 -3. For other devices: Add device name to database in `cc_conf.c` - -### Problem: Too much padding on rectangular display - -**Cause**: Incorrectly detected as circular - -**Solution Options**: -1. **Quick fix (recommended)**: Set `shape` to `"rectangular"` in `config.json` -2. For NZXT Kraken: Verify resolution is ≤240×240 -3. For other devices: Ensure not in circular device database - -### Problem: Unknown device defaults to rectangular - -**Cause**: Not in database and not NZXT Kraken - -**Solution**: This is intentional (safe default). Set `shape` to `"circular"` in `config.json` if needed. - -### Testing Shape Override - -```bash -# Test rectangular layout - set in config.json: "shape": "rectangular" -sudo systemctl restart coolerdash.service - -# Test circular layout - set in config.json: "shape": "circular" -sudo systemctl restart coolerdash.service - -# Check logs for inscribe factor -journalctl -u coolerdash.service -f | grep "inscribe" -``` - -Expected output with manual override: -``` -[INFO] Display shape forced to rectangular (inscribe factor: 1.0000) -[INFO] Display shape forced to circular (inscribe factor: 0.7071) -``` - - -### Problem: Display Detected Incorrectly - -**Solution for Circular display treated as rectangular:** -- Add device name to database in `cc_conf.c` - -**Solution for Rectangular display treated as circular:** -1. Check if device name is incorrectly in `circular_devices[]` -2. Remove from database - -## Implementation Details - -### Constants - -```c -#define M_SQRT1_2 0.7071067811865476 // 1/√2 (inscribe factor) -#define CONTENT_SCALE_FACTOR 0.98 // Bars use 98% of safe area -#define TEMP_EDGE_MARGIN_FACTOR 0.02 // 2% margin for temperature labels -``` - -### Scaling Flow - -1. **Device Detection**: `is_circular_display_device()` checks device name and resolution -2. **Inscribe Factor**: Set to 0.7071 (circular) or 1.0 (rectangular) -3. **Safe Area Calculation**: `safe_width = display_width × inscribe_factor` -4. **Content Scaling**: `bar_width = safe_width × CONTENT_SCALE_FACTOR` - ---- - -**Last Updated**: After resolution-based NZXT Kraken detection implementation +| Problem | Solution | +|---------|----------| +| Clipping on circular display | Set `"shape": "circular"` in config | +| Too much padding on rectangular | Set `"shape": "rectangular"` in config | +| Unknown device | Set `shape` manually or add to database | diff --git a/docs/display-modes.md b/docs/display-modes.md index c7fcc12..1c9cbd6 100644 --- a/docs/display-modes.md +++ b/docs/display-modes.md @@ -1,827 +1,127 @@ -# CoolerDash Display Modes - Developer Guide +# Display Modes -## Table of Contents -- [Overview](#overview) -- [Architecture](#architecture) -- [Dual Mode](#dual-mode) -- [Circle Mode](#circle-mode) -- [Configuration System](#configuration-system) -- [Rendering Pipeline](#rendering-pipeline) -- [Display Detection](#display-detection) -- [Adding New Modes](#adding-new-modes) +Two modes: **dual** (default) and **circle**. ---- - -## Overview - -CoolerDash supports two distinct display modes for rendering temperature information on LCD screens: - -- **Dual Mode** (`dual`): Simultaneous display of CPU and GPU temperatures with side-by-side layout -- **Circle Mode** (`circle`): Alternating single-sensor display optimized for circular high-resolution screens - -### Mode Selection - -The mode is selected through a two-tier configuration system: - -1. **System Default** (`src/device/config.c`): `display_mode = "dual"` -2. **User Configuration** (`/etc/coolercontrol/plugins/coolerdash/config.json`): `"mode": "dual"|"circle"` - ---- +Mode selection: `config.json` → `"display": { "mode": "dual" }` or CLI `--dual` / `--circle`. -## Architecture - -### File Structure +## Files ``` src/mods/ -├── display.c # Mode dispatcher -├── display.h # Dispatcher API -├── dual.c # Dual mode implementation -├── dual.h # Dual mode API -├── circle.c # Circle mode implementation -└── circle.h # Circle mode API +├── display.c/h # Mode dispatcher +├── dual.c/h # Dual mode +└── circle.c/h # Circle mode ``` -### Mode Dispatcher - -The mode dispatcher is located in `src/mods/display.c`: - +Dispatcher (`display.c`): ```c -void draw_display_image(const struct Config *config) -{ - if (strcmp(config->display_mode, "circle") == 0) { +void draw_display_image(const struct Config *config) { + if (strcmp(config->display_mode, "circle") == 0) draw_circle_image(config); - } else { + else draw_dual_image(config); - } } ``` -This function is called by the main monitoring loop and routes to the appropriate mode implementation. - --- ## Dual Mode -### Purpose -Displays CPU and GPU temperatures simultaneously in a side-by-side layout. - -### Implementation File -`src/mods/dual.c` (602 lines) - -### Key Components - -#### 1. ScalingParams Structure -```c -typedef struct { - double scale_x; // Horizontal scaling factor - double scale_y; // Vertical scaling factor - double corner_radius; // Bar corner radius - double inscribe_factor; // 1.0 (rectangular) or M_SQRT1_2 (circular) - int safe_bar_width; // Safe bar width for circular displays - double safe_content_margin; // Horizontal margin - int is_circular; // Display type flag -} ScalingParams; -``` - -#### 2. Core Functions - -##### `calculate_scaling_params()` -Calculates dynamic scaling based on display dimensions: -- Base resolution: 240x240px -- Checks `shape` config override (rectangular/circular/auto) -- Falls back to device detection for circular displays -- Applies inscribe factor (1/√2 ≈ 0.7071) for circular displays -- **Uses configurable `content_scale_factor`** (0.5-1.0, default: 0.98) to determine safe area percentage -- Calculates safe content area to avoid edge clipping - -**Priority system:** -1. `shape` config parameter (manual override) -2. `force_display_circular` flag (deprecated) -3. Automatic device detection (default) - -**Content Scale Configuration:** -```ini -[display] -content_scale_factor=0.98 # 0.5-1.0, default: 0.98 (2% margin) -``` - -**Scale Factor Impact:** -- **0.98-1.0:** Minimal margins, maximum screen usage -- **0.90-0.97:** Comfortable padding, safe text rendering -- **0.70-0.89:** Extra margins, conservative layout -- **0.5-0.69:** Large margins, centered content focus - -##### `draw_dual_bars()` -Renders CPU and GPU temperature bars: -- Left bar: CPU temperature -- Right bar: GPU temperature -- Color-coded based on temperature thresholds -- Rounded corners with configurable radius -- Border and background colors from config - -##### `draw_temperature_labels()` -Renders temperature values and labels: -- Displays numeric temperature with degree symbol -- Draws "CPU" and "GPU" labels -- Uses configurable fonts and colors -- Dynamically positions based on scaling parameters - -##### `render_dual_display()` -Main rendering function: -- Creates Cairo surface and context -- Draws black background -- Calls component rendering functions -- Writes PNG to filesystem -- Uploads image to LCD device - -### Layout Algorithm +CPU and GPU temperatures side-by-side. ``` ┌────────────────────────────┐ -│ CPU: 45° GPU: 52° │ ← Temperature values -│ ┌─────┐ ┌─────┐ │ -│ │█████│ │██████│ │ ← Color-coded bars -│ └─────┘ └─────┘ │ -│ CPU GPU │ ← Labels +│ CPU: 45° GPU: 52° │ +│ ┌─────┐ ┌─────┐ │ +│ │█████│ │██████│ │ +│ └─────┘ └─────┘ │ +│ CPU GPU │ └────────────────────────────┘ ``` -### Circular Display Handling - -For circular displays, dual mode applies an inscribe factor: -- Safe area width = `display_width × M_SQRT1_2 × 0.98` -- Horizontal centering with margin calculation -- Prevents content from clipping at display edges - ---- - -## Circle Mode - -### Purpose -Alternates between CPU and GPU display every 5 seconds, optimized for high-resolution circular screens where dual mode scaling is not optimal. - -### Implementation File -`src/mods/circle.c` (456 lines) - -### Key Components - -#### 1. SensorMode - -Circle mode cycles through the three sensor slots configured in `config.json`: - -```json -"display": { - "sensor_slot_1": "cpu", - "sensor_slot_2": "liquid", - "sensor_slot_3": "gpu" -} -``` - -The slots are `cpu`, `gpu`, or `liquid`. Each slot is displayed full-screen in turn. +### ScalingParams -#### 2. Global State Management ```c -static int current_slot = 0; // cycles 0→1→2→0 -static time_t last_switch_time = 0; -``` - -#### 3. Configuration-Based Timing - -The sensor switch interval is **configurable** via `config.json`: - -```c -// No longer hardcoded - uses Config parameter -void update_sensor_mode(const struct Config *config) -{ - time_t current_time = time(NULL); - - // Use circle_switch_interval from config (default: 5 seconds) - if (difftime(current_time, last_switch_time) >= config->circle_switch_interval) { - current_sensor = (current_sensor == SENSOR_CPU) ? SENSOR_GPU : SENSOR_CPU; - last_switch_time = current_time; - } -} +typedef struct { + double scale_x, scale_y; + double corner_radius; + double inscribe_factor; // 1.0 (rectangular) or M_SQRT1_2 (circular) + int safe_bar_width; + double safe_content_margin; + int is_circular; +} ScalingParams; ``` -**Configuration (`config.json`):** -```json -{ - "display": { - "mode": "circle", - "circle_switch_interval": 5 - } -} - -**Use Cases:** -- **Fast (1-3s):** Quick sensor overview -- **Moderate (5-8s):** Balanced viewing (default: 5s) -- **Slow (10-60s):** Focus on individual sensors - -#### 4. Core Functions +Base resolution: 240×240. Scales dynamically. +Circular displays: `safe_area = display_width × inscribe_factor × content_scale_factor` -##### `update_sensor_mode(const struct Config *config)` -Manages sensor alternation: -- Uses `time()` with configurable interval from `config->circle_switch_interval` -- Toggles between CPU and GPU -- Logs sensor switches for debugging +### Rendering Flow -##### `draw_single_sensor()` -Renders single sensor display: -- **Bar-Centered Layout**: Uses temperature bar as central reference point -- **Temperature Positioning**: 55% of display height above bar -- **Label Positioning**: 5% of display height below bar -- **Centering Algorithm**: Calculates total width including degree symbol for proper visual balance +1. `draw_dual_image()` — entry point +2. `get_cached_lcd_device_data()` — device info +3. `get_temperature_monitor_data()` — sensor data +4. Cairo: create surface → draw background → draw bars + labels → write PNG +5. `send_image_to_lcd()` — upload -##### `calculate_temp_fill_width()` -Calculates temperature bar fill: -- Bounds-checked ratio calculation -- Uses unified `temp_max_scale` from config -- Returns pixel width for bar fill - -##### `get_temperature_bar_color()` -Determines bar color based on thresholds: -- `temp_threshold_1`: Lowest (blue/green) -- `temp_threshold_2`: Medium-low (yellow) -- `temp_threshold_3`: Medium-high (orange) -- `temp_threshold_4`: Critical (red) +--- -##### `render_circle_display()` -Main rendering function: -- Creates Cairo surface and context -- Updates sensor mode (checks 5s interval) -- Renders current sensor -- Writes PNG and uploads to LCD +## Circle Mode -### Layout Algorithm +Alternates between sensor slots, one at a time. Optimized for circular high-res displays. ``` ┌────────────────┐ -│ │ -│ 45° │ ← Temperature (55% above bar) -│ │ -│ ┌──────┐ │ ← Temperature bar (centered) +│ 45° │ +│ ┌──────┐ │ │ │██████│ │ │ └──────┘ │ -│ │ -│ CPU │ ← Label (5% below bar) -│ │ +│ CPU │ └────────────────┘ ``` -### Centering Algorithm - -Temperature centering includes degree symbol width: -```c -// Calculate degree symbol width -cairo_text_extents_t degree_ext; -cairo_text_extents(cr, "°", °ree_ext); - -// Total width = temperature + spacing + degree -const double total_width = temp_ext.width + 5 + degree_ext.width; - -// Center as a unit -double temp_x = (config->display_width - total_width) / 2.0; -``` - -This ensures visual balance by treating "45°" as a single unit rather than centering only the number. - -### Timing Implementation +### Sensor Slots -The sensor switching interval is **configurable** via `config.json` (1-60 seconds, default: 5): - -```c -void update_sensor_mode(const struct Config *config) -{ - time_t current_time = time(NULL); - - // Use configurable interval from config.json - if (difftime(current_time, last_switch_time) >= config->circle_switch_interval) { - current_sensor = (current_sensor == SENSOR_CPU) ? SENSOR_GPU : SENSOR_CPU; - last_switch_time = current_time; - } -} -``` - -**Configuration (`config.json`):** +Configured in `config.json`: ```json -{ - "display": { - "mode": "circle", - "circle_switch_interval": 5 - } -} - -**Note**: For sub-second precision, `nanosleep()` or `clock_gettime()` could be used, but the current implementation provides sufficient accuracy for display purposes. - ---- - -## Configuration System - -### Config Structure Extension - -The `Config` structure in `src/device/config.h` includes: -```c -char display_mode[16]; // "dual" or "circle" -``` - -### System Defaults (`src/device/config.c`) - -```c -static void set_display_defaults(struct Config *config) -{ - // ... other defaults ... - cc_safe_strcpy(config->display_mode, sizeof(config->display_mode), "dual"); -} -``` - -### JSON Parsing (`src/device/config.c`) - -```c -static void load_display_from_json(Config *config, json_t *display) -{ - json_t *val = json_object_get(display, "mode"); - if (json_is_string(val)) { - const char *mode = json_string_value(val); - if (strcmp(mode, "dual") == 0 || strcmp(mode, "circle") == 0) - SAFE_STRCPY(config->display_mode, mode); - } -} - -### CLI Override (`src/main.c`) - -```c -static void parse_arguments(int argc, char *argv[], - char *display_mode_override, size_t override_size) -{ - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--dual") == 0) { - cc_safe_strcpy(display_mode_override, "dual", override_size); - } else if (strcmp(argv[i], "--circle") == 0) { - cc_safe_strcpy(display_mode_override, "circle", override_size); - } - } -} - -int main(int argc, char *argv[]) -{ - char display_mode_override[16] = {0}; - parse_arguments(argc, argv, display_mode_override, sizeof(display_mode_override)); - - if (display_mode_override[0] != '\0') { - cc_safe_strcpy(config.display_mode, display_mode_override, - sizeof(config.display_mode)); - } -} -``` - ---- - -## Rendering Pipeline - -### 1. Main Loop (`src/main.c`) -```c -while (1) { - draw_display_image(&config); // Mode dispatcher - sleep(update_interval); -} +"sensor_slot_1": "cpu", +"sensor_slot_2": "liquid", +"sensor_slot_3": "gpu" ``` -### 2. Mode Dispatcher (`src/mods/display.c`) -```c -void draw_display_image(const struct Config *config) -{ - if (strcmp(config->display_mode, "circle") == 0) { - draw_circle_image(config); - } else { - draw_dual_image(config); - } -} -``` - -### 3. Mode-Specific Rendering - -#### Dual Mode Flow: -1. `draw_dual_image()` → Entry point -2. `get_cached_lcd_device_data()` → Device information -3. `get_temperature_monitor_data()` → Sensor data -4. `render_dual_display()` → Cairo rendering -5. `cairo_surface_write_to_png()` → PNG generation -6. `send_image_to_lcd()` → LCD upload +Cycles through slots at `circle_switch_interval` (default: 8s, range: 1–60s). -#### Circle Mode Flow: -1. `draw_circle_image()` → Entry point -2. `get_cached_lcd_device_data()` → Device information -3. `get_temperature_monitor_data()` → Sensor data -4. `update_sensor_mode()` → Check 5s interval -5. `render_circle_display()` → Cairo rendering -6. `cairo_surface_write_to_png()` → PNG generation -7. `send_image_to_lcd()` → LCD upload +### State -### 4. Common Cairo Operations - -Both modes use identical Cairo workflow: ```c -// 1. Create surface -cairo_surface_t *surface = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, width, height); - -// 2. Create context -cairo_t *cr = cairo_create(surface); - -// 3. Draw content -cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); // Black background -cairo_paint(cr); -// ... mode-specific rendering ... - -// 4. Write PNG -cairo_surface_write_to_png(surface, path); - -// 5. Cleanup -cairo_destroy(cr); -cairo_surface_destroy(surface); -``` - ---- - -## Display Detection - -### Device Name Detection - -Both modes use `is_circular_display_device()` to detect circular displays: - -```c -int is_circular = is_circular_display_device(device_name, width, height); -``` - -**Known Circular Devices**: -- NZXT Kraken Elite (240x240) -- Corsair iCUE LINK (Other models to be added) - -### Display Shape Override (New in v1.96) - -**Recommended Method:** Manual configuration override in `config.json`: -```json -{ - "display": { - "shape": "auto" - } -} -``` -> Values: `"auto"` (default), `"rectangular"`, `"circular"` — `"circular"` inscribe factor is configurable via `display_inscribe_factor` (default: 0.70710678; 0.0 = auto). - -**Priority System:** -1. `shape` config parameter (highest - manual override) -2. Automatic device detection (default) - -**Use cases:** -- Testing circular layout on rectangular displays -- Fixing incorrect auto-detection results -- Devices not in the detection database -- Custom/modded hardware - -### Inscribe Factor Application - -For circular displays: -```c -if (is_circular) { - // `params->inscribe_factor` is set to either the configured - // `display_inscribe_factor` (>0 && <=1) or falls back to the - // geometric inscribe factor M_SQRT1_2 (1/√2 ≈ 0.7071) - params->inscribe_factor = cfg_inscribe; - safe_area_width = display_width * inscribe_factor; -} else { - params->inscribe_factor = 1.0; - safe_area_width = display_width; -} -``` - -This ensures content fits within the visible circular area without clipping. - ---- - -## Adding New Modes - -### Step-by-Step Guide - -#### 1. Create Mode Files - -Create `src/mods/newmode.c` and `src/mods/newmode.h`: - -```c -// newmode.h -#ifndef NEWMODE_H -#define NEWMODE_H - -#include "../device/config.h" - -void draw_newmode_image(const struct Config *config); - -#endif -``` - -#### 2. Implement Rendering - -```c -// newmode.c -#include "newmode.h" -#include - -void draw_newmode_image(const struct Config *config) -{ - // 1. Get device info - char device_uid[128] = {0}; - char device_name[128] = {0}; - int screen_width = 0, screen_height = 0; - - if (!get_cached_lcd_device_data(config, device_uid, sizeof(device_uid), - device_name, sizeof(device_name), - &screen_width, &screen_height)) { - return; - } - - // 2. Get sensor data - monitor_sensor_data_t data = {0}; - if (!get_temperature_monitor_data(config, &data)) { - return; - } - - // 3. Create Cairo context - cairo_surface_t *surface = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, config->display_width, config->display_height); - cairo_t *cr = cairo_create(surface); - - // 4. Draw your custom content - cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); - cairo_paint(cr); - // ... your rendering code ... - - // 5. Write PNG - cairo_surface_write_to_png(surface, config->paths_image_coolerdash); - cairo_destroy(cr); - cairo_surface_destroy(surface); - - // 6. Upload to LCD - send_image_to_lcd(config, config->paths_image_coolerdash, device_uid); -} -``` - -#### 3. Update Mode Dispatcher - -In `src/mods/dual.c`, add your mode: - -```c -#include "newmode.h" - -void draw_display_image(const struct Config *config) -{ - if (strcmp(config->display_mode, "newmode") == 0) { - draw_newmode_image(config); - } else if (strcmp(config->display_mode, "circle") == 0) { - draw_circle_image(config); - } else { - draw_dual_image(config); - } -} -``` - -#### 4. Update Makefile - -```makefile -SRC_MODULES = src/mods/dual.c src/mods/circle.c src/mods/newmode.c -HEADERS = ... src/mods/newmode.h -``` - -#### 5. Update Configuration - -Add validation in `src/device/config.c`: - -```c -if (strcmp(mode, "dual") == 0 || - strcmp(mode, "circle") == 0 || - strcmp(mode, "newmode") == 0) { - SAFE_STRCPY(config->display_mode, mode); -} -``` - -#### 6. Add CLI Flag - -In `src/main.c`: - -```c -// In parse_arguments() -else if (strcmp(argv[i], "--newmode") == 0) { - cc_safe_strcpy(display_mode_override, "newmode", override_size); -} - -// In show_help() -printf(" --newmode Use new display mode\n"); -``` - -#### 7. Update Documentation - -- `README.md`: Add mode description -- `/etc/coolercontrol/plugins/coolerdash/config.json`: Add example -- `docs/display-modes.md`: Add technical details - ---- - -## Best Practices - -### 1. Cairo Resource Management - -Always pair creation with destruction: -```c -cairo_t *cr = cairo_create(surface); -// ... use cr ... -cairo_destroy(cr); // Must call - -cairo_surface_t *surface = cairo_image_surface_create(...); -// ... use surface ... -cairo_surface_destroy(surface); // Must call -``` - -### 2. Configuration Validation - -Validate all config values before use: -```c -if (config->display_width <= 0 || config->display_height <= 0) { - log_message(LOG_ERROR, "Invalid display dimensions"); - return; -} -``` - -### 3. Error Handling - -Check all Cairo operations: -```c -if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { - log_message(LOG_ERROR, "Surface creation failed"); - return; -} -``` - -### 4. Logging - -Use appropriate log levels: -- `LOG_ERROR`: Critical failures -- `LOG_WARNING`: Recoverable issues -- `LOG_INFO`: Mode switches, state changes -- `LOG_STATUS`: Successful operations -- `LOG_DEBUG`: Detailed tracing - -### 5. Null Checks - -Always validate pointers: -```c -if (!cr || !config || !data) { - log_message(LOG_ERROR, "Null parameter"); - return; -} -``` - -### 6. Scaling Calculations - -Use dynamic scaling for resolution independence: -```c -ScalingParams params = calculate_scaling_params(config); - -// Use configurable content_scale_factor (default: 0.98) -double content_scale = (config->display_content_scale_factor > 0.5 && - config->display_content_scale_factor <= 1.0) ? - config->display_content_scale_factor : 0.98; - -// Apply inscribe factor for circular displays -double inscribe = params.inscribe_factor; // 1.0 or M_SQRT1_2 (≈0.7071) - -// Calculate safe area -double safe_width = config->display_width * content_scale * inscribe; -double safe_height = config->display_height * content_scale * inscribe; -``` - -**Configuration:** -```ini -[display] -content_scale_factor=0.98 # 0.5-1.0, controls margin/padding -``` -```c -const double scale_avg = (scale_x + scale_y) / 2.0; -const double corner_radius = 8.0 * scale_avg; -``` - ---- - -## Performance Considerations - -### 1. Cairo Surface Reuse -Consider reusing surfaces for reduced memory allocation: -```c -static cairo_surface_t *cached_surface = NULL; - -if (!cached_surface) { - cached_surface = cairo_image_surface_create(...); -} -``` - -### 2. PNG Compression -Cairo uses default PNG compression. For faster writes: -```c -// Trade file size for speed (not currently implemented) -// Could add custom PNG write with lower compression -``` - -### 3. LCD Upload Frequency -Respect device limits: -- NZXT Kraken: ~1-2 FPS recommended -- Adjust `update_interval` based on device - -### 4. Circle Mode Timing -Current implementation uses `time()` for 5s intervals: -- Granularity: 1 second -- Suitable for display purposes -- For sub-second precision, consider `clock_gettime()` - ---- - -## Debugging Tips - -### 1. Enable Debug Logging - -Compile with `-DDEBUG`: -```bash -gcc -g -O0 -DDEBUG src/*.c -o coolerdash -lcurl -lcairo -lm -ljansson -``` - -### 2. Test PNG Output - -Check generated PNG manually: -```bash -feh /tmp/coolerdash.png -``` - -### 3. Mode Verification - -Log current mode on startup: -```c -log_message(LOG_INFO, "Display mode: %s", config->display_mode); -``` - -### 4. Circle Mode State - -Monitor sensor switches: -```c -log_message(LOG_INFO, "Circle mode: switched to %s", - current_sensor == SENSOR_CPU ? "CPU" : "GPU"); +static int current_slot = 0; // 0→1→2→0 +static time_t last_switch_time = 0; ``` -### 5. Cairo Status +### Centering -Always check status after operations: +Temperature + degree symbol centered as a unit: ```c -cairo_status_t status = cairo_status(cr); -if (status != CAIRO_STATUS_SUCCESS) { - log_message(LOG_ERROR, "Cairo error: %s", cairo_status_to_string(status)); -} +const double total_width = temp_ext.width + 5 + degree_ext.width; +double temp_x = (display_width - total_width) / 2.0; ``` ---- - -## Future Enhancements +### Rendering Flow -### Potential Features - -1. **Graph Mode**: Temperature history graphs -2. **Minimal Mode**: Single temperature value only -3. **Animation Mode**: Smooth transitions between values -4. **Custom Layouts**: User-defined positioning via config -5. **Multi-Sensor**: Support for more than 2 sensors -6. **Themes**: Predefined color schemes - -### Implementation Considerations - -- Mode plugin architecture for runtime loading -- JSON configuration for complex layouts -- Animation frame buffering -- Custom font support (TTF loading) -- Image overlays (logos, backgrounds) +1. `draw_circle_image()` — entry point +2. `get_cached_lcd_device_data()` — device info +3. `get_temperature_monitor_data()` — sensor data +4. `update_sensor_mode()` — check switch interval +5. Cairo: create surface → draw single sensor → write PNG +6. `send_image_to_lcd()` — upload --- -## Conclusion - -CoolerDash's display mode architecture provides a flexible framework for LCD rendering. The dual and circle modes demonstrate different approaches to the same problem, each optimized for specific use cases. - -Key takeaways: -- **Dual Mode**: Best for rectangular displays or when both sensors need simultaneous visibility -- **Circle Mode**: Optimal for high-resolution circular displays where space is limited -- **Mode System**: Three-tier configuration with CLI override capability -- **Cairo Integration**: Consistent rendering pipeline across all modes -- **Extensibility**: Clear pattern for adding new modes - -For questions or contributions, see `CONTRIBUTING.md`. - ---- +## Adding a New Mode -**Version**: 1.0 -**Last Updated**: November 6, 2025 -**Authors**: damachine +1. Create `src/mods/newmode.c/h` +2. Implement `draw_newmode_image(const struct Config *config)` +3. Add dispatch in `display.c` +4. Add validation in `config.c` (`load_display_from_json`) +5. Add to Makefile `SRC_MODULES` +6. Add CLI flag in `main.c` (`parse_arguments`) +7. Update docs and `config.json` diff --git a/docs/plugin-integration.md b/docs/plugin-integration.md index d513a2e..c0aed99 100644 --- a/docs/plugin-integration.md +++ b/docs/plugin-integration.md @@ -1,14 +1,10 @@ -# CoolerDash Plugin Integration for CoolerControl +# Plugin Integration -## Overview +CoolerDash runs as a CoolerControl plugin managed by `cc-plugin-coolerdash.service`. No separate systemd service needed. -CoolerDash runs as a native CoolerControl plugin managed by `cc-plugin-coolerdash.service`. No separate systemd service or startup delay is needed. CoolerControl handles the plugin lifecycle. +## Authentication -## CC4 Integration - -### Authentication - -CC4 uses Bearer Token authentication. Generate a token in CoolerControl UI under **Access Protection** and set it in `config.json`: +Bearer token auth via CoolerControl Access Protection: ```json "daemon": { @@ -16,49 +12,30 @@ CC4 uses Bearer Token authentication. Generate a token in CoolerControl UI under } ``` -### Shutdown Image +Generate token in CoolerControl UI > Access Protection. + +## Shutdown Image -CoolerDash registers `shutdown.png` with CC4 once at startup: +Registered once at startup via CC4 API: ``` PUT /devices/{uid}/settings/lcd/lcd/shutdown-image ``` -CC4 stores the image server-side and displays it when CoolerControl stops. No helper daemon or `ExecStop` workaround needed. +CC4 stores the image and displays it when CoolerControl stops. No helper daemon needed. ## Plugin UI -### Theme-Adaptive UI +Theme-adaptive UI using CoolerControl CSS variables + Tailwind CSS + PrimeIcons. -The plugin UI automatically adapts to the user's CoolerControl theme: - -- **Dark/Light Theme Support** - Seamlessly integrates with user preferences -- **Theme Color Variables** - Uses CoolerControl's native color system -- **Consistent Design** - Matches CoolerControl's visual language - -#### CSS Variables Used +### CSS Variables ```css --colors-bg-one /* Primary background */ --colors-bg-two /* Secondary background */ --colors-border-one /* Border color */ --colors-text /* Text color */ ---colors-accent /* Accent/highlight color */ -``` - -### Tailwind CSS - -```html -
- -
-``` - -### PrimeIcons - -```html - - +--colors-accent /* Accent color */ ``` ### Manifest @@ -68,109 +45,16 @@ version = "{{VERSION}}" url = "https://github.com/damachine/coolerdash" ``` -These fields are displayed on the CoolerControl plugin page, helping users: -- Check if their plugin is up-to-date -- Report issues or request features -- Access documentation +Displayed on the CoolerControl plugin page. -## Plugin UI Structure +## Settings UI Sections -### Before (2.x and earlier) +- Daemon: API address, access token +- Display: Mode, refresh interval, brightness, orientation +- Advanced: Display dimensions -- ❌ Hardcoded colors -- ❌ Separate helperd service for shutdown -- ❌ Custom CSS only +## Related -### Current (3.x) - -- ✅ Theme-adaptive colors via CSS variables -- ✅ Tailwind CSS -- ✅ Shutdown handled natively by CC4 -- ✅ PrimeIcons - -## Configuration UI - -The plugin settings page includes: - -### 🌐 Daemon Settings -- CoolerControl API Address -- Access Token - -### 🖥️ Display Mode -- Mode selection (Dual/Circle) -- Circle switch interval - -### 📊 Display Settings -- Refresh interval -- Brightness slider (with live preview) -- Orientation selector - -### 🔧 Advanced Settings -- Display dimensions (width/height) - -## Technical Implementation - -### Theme Colors Example - -```css -.section-card { - background: rgb(var(--colors-bg-one)); - border: 1px solid rgb(var(--colors-border-one)); - border-radius: 0.5rem; - padding: 1.5rem; -} - -.input-field:focus { - border-color: rgb(var(--colors-accent)); -} -``` - -### Responsive Design - -```html - -
- -
- -``` - -## Benefits - -### For Users - -1. **Visual Consistency** - Plugin UI matches CoolerControl's appearance -2. **Theme Support** - Automatically adapts to dark/light themes -3. **Better UX** - Standard UI patterns and behaviors -4. **Version Info** - Easy to check if plugin is up-to-date - -### For Developers - -1. **Rapid Development** - Tailwind CSS speeds up UI work -2. **Maintainability** - Theme variables reduce hardcoded values -3. **Documentation** - Clear examples and patterns -4. **Best Practices** - Following CoolerControl's UI guidelines - -## Migration Guide - -If you're updating from an older version: - -1. **No User Action Required** - UI changes are automatic -2. **Config Preserved** - Existing settings remain intact -3. **Theme Applies Immediately** - UI adapts to current theme - -## Resources - -- [Plugin UI Theming Guide](./plugin-ui-theming.md) +- [Configuration Guide](config-guide.md) +- [Plugin UI Theming](plugin-ui-theming.md) - [CoolerControl Custom Device Plugin](https://gitlab.com/coolercontrol/cc-plugin-custom-device) -- [index.html](../etc/coolercontrol/plugins/coolerdash/index.html) - Full implementation - -## Acknowledgments - -Special thanks to @codifryed (CoolerControl developer) for the custom-device plugin reference and CC4 API design. - -## Related Documentation - -- [Configuration Guide](./config-guide.md) -- [Developer Guide](./developer-guide.md) -- [Display Modes](./display-modes.md) diff --git a/docs/plugin-ui-theming.md b/docs/plugin-ui-theming.md index 2b38863..fe3494d 100644 --- a/docs/plugin-ui-theming.md +++ b/docs/plugin-ui-theming.md @@ -1,29 +1,19 @@ -# CoolerDash Plugin UI Theming Guide +# Plugin UI Theming -## Overview +The plugin UI adapts to CoolerControl's theme (dark/light) via CSS variables. -CoolerDash's plugin UI is designed to seamlessly integrate with CoolerControl's theme system. This document explains how the UI adapts to user themes and the available customization options. - -## Theme Integration - -### Available CSS Variables - -CoolerControl provides the following CSS variables that automatically adapt to the user's selected theme: - -#### Color Variables +## CSS Variables ```css -rgb(var(--colors-bg-one)) /* Primary background color */ -rgb(var(--colors-bg-two)) /* Secondary background color */ -rgb(var(--colors-border-one)) /* Border color */ -rgb(var(--colors-text)) /* Primary text color */ -rgb(var(--colors-accent)) /* Accent color (highlights, buttons) */ -rgb(var(--colors-red)) /* Red color (errors, warnings) */ +rgb(var(--colors-bg-one)) /* Primary background */ +rgb(var(--colors-bg-two)) /* Secondary background */ +rgb(var(--colors-border-one)) /* Border */ +rgb(var(--colors-text)) /* Text */ +rgb(var(--colors-accent)) /* Accent / highlights */ +rgb(var(--colors-red)) /* Errors */ ``` -### Usage Examples - -#### Basic Container +## Usage ```css .section-card { @@ -32,160 +22,71 @@ rgb(var(--colors-red)) /* Red color (errors, warnings) */ border-radius: 0.5rem; padding: 1.5rem; } -``` - -#### Input Fields - -```css -.input-field { - background: rgb(var(--colors-bg-one)); - border: 1px solid rgb(var(--colors-border-one)); - color: rgb(var(--colors-text)); -} .input-field:focus { border-color: rgb(var(--colors-accent)); } -``` - -#### Buttons -```css .btn-primary { background: rgb(var(--colors-accent)); - color: white; opacity: 0.8; } - .btn-primary:hover { opacity: 1; } ``` -## Tailwind CSS Support - -CoolerControl provides Tailwind CSS classes for rapid UI development: - -### Layout Classes - -- `flex`, `flex-col`, `flex-row` - Flexbox layouts -- `grid`, `grid-cols-2` - Grid layouts -- `p-2`, `p-4`, `px-2`, `py-4` - Padding -- `m-2`, `m-4`, `mx-auto` - Margins -- `gap-2`, `gap-4` - Gaps in flex/grid - -### Typography - -- `text-sm`, `text-lg`, `text-2xl` - Font sizes -- `font-bold`, `font-semibold` - Font weights -- `opacity-70` - Opacity adjustments +Always use theme variables instead of hardcoded colors. -### Colors +## Tailwind CSS -- `bg-bg-one`, `bg-bg-two` - Background colors -- `text-text-color` - Text color -- `border-border-one` - Border color +Available utility classes: -### Responsive Design +| Class | Role | +|-------|------| +| `bg-bg-one` | Base / deepest background layer | +| `bg-bg-two` | Elevated surface — panels, cards, dialogs | +| `bg-surface-hover` | Subtle overlay for hover states | +| `bg-accent` / `text-accent` | Brand / interactive accent color | +| `text-text-color` | Primary text | +| `text-text-color-secondary` | Muted / secondary text | +| `border-border-one` | Standard border color | +| `bg-success` / `text-success` | Success (green) | +| `bg-error` / `text-error` | Error / danger (red) | +| `bg-warning` / `text-warning` | Warning (yellow) | -- `min-h-full` - Minimum height -- `max-w-4xl` - Maximum width -- `rounded-lg` - Border radius +Layout: `flex`, `flex-col`, `grid`, `p-2`, `p-4`, `gap-2`, `text-sm`, `font-bold`, `rounded-lg`. -## PrimeIcons Support - -CoolerControl includes PrimeIcons for consistent iconography: +## PrimeIcons ```html - - - - -``` - -## Best Practices - -### 1. Always Use Theme Variables - -❌ **Don't:** -```css -background: #1a1a2e; -color: #eaeaea; -border: 1px solid #2d4263; + + + + ``` -✅ **Do:** -```css -background: rgb(var(--colors-bg-one)); -color: rgb(var(--colors-text)); -border: 1px solid rgb(var(--colors-border-one)); -``` +## Rendering Context -### 2. Provide Visual Feedback - -```css -.input-field:focus { - border-color: rgb(var(--colors-accent)); -} +Detect how the plugin UI is displayed: -.btn:hover { - opacity: 1; -} +```js +const { mode } = await getContext(); // 'modal' | 'full_page' ``` -### 3. Use Semantic Color Meanings - -- `--colors-accent` - Primary actions, highlights -- `--colors-red` - Errors, destructive actions -- `--colors-bg-one` - Content containers -- `--colors-bg-two` - Page background - -### 4. Maintain Consistency +- `modal` — opened as a dialog (e.g. settings shortcut) +- `full_page` — dedicated plugin page in the sidebar -Use the same spacing, border-radius, and transition patterns throughout: - -```css -border-radius: 0.375rem; /* For inputs/buttons */ -border-radius: 0.5rem; /* For containers */ -transition: opacity 0.2s; /* For hover effects */ -``` - -## Dark/Light Theme Support - -The UI automatically adapts to the user's theme (dark/light) through the CSS variables. No additional JavaScript or CSS is needed. - -## Testing - -To test your UI with different themes: - -1. Open CoolerControl settings -2. Change the theme under Appearance -3. Navigate to CoolerDash plugin settings -4. Verify all UI elements adapt correctly - -## Example Implementation - -See [index.html](../etc/coolercontrol/plugins/coolerdash/index.html) for a complete implementation example. - -## Resources - -- [CoolerControl Custom Device Plugin](https://gitlab.com/coolercontrol/cc-plugin-custom-device) - Reference implementation -- [Tailwind CSS Documentation](https://tailwindcss.com/docs) -- [PrimeIcons](https://primevue.org/icons/) - Icon reference - -## Manifest Configuration - -Add version and URL to your `manifest.toml`: - -```toml -version = "2.2.x" -url = "https://github.com/damachine/coolerdash" -``` +## Semantic Colors -These will be displayed on the plugin page in CoolerControl's UI. +- `--colors-accent` — primary actions, highlights +- `--colors-red` — errors, destructive actions +- `--colors-bg-one` — content containers +- `--colors-bg-two` — page background -## Changelog +## Reference -- **3.x** - Token-only CC4 auth, native shutdown image via CC4 API -- **2.2.x** - Added theme color support and Tailwind CSS integration -- **2.0.4** - Initial plugin UI implementation +- Implementation: [index.html](../etc/coolercontrol/plugins/coolerdash/ui/index.html) +- [CoolerControl Plugin Docs](https://gitlab.com/coolercontrol/cc-plugins) +- [Tailwind CSS Docs](https://tailwindcss.com/docs) +- [PrimeIcons](https://primevue.org/icons/) diff --git a/etc/coolercontrol/plugins/coolerdash/config.json b/etc/coolercontrol/plugins/coolerdash/config.json index 9d3c34e..19a480a 100644 --- a/etc/coolercontrol/plugins/coolerdash/config.json +++ b/etc/coolercontrol/plugins/coolerdash/config.json @@ -3,8 +3,7 @@ "daemon": { "address": "http://localhost:11987", - "access_token": "", - "_comment_token": "Create a token in CoolerControl UI → Access Protection. Format: cc_." + "access_token": "" }, "device_detection": { @@ -22,9 +21,9 @@ "display": { "mode": "circle", - "circle_switch_interval": 5, + "circle_switch_interval": 8, "circle_show_extra_info": true, - "refresh_interval": 2.5, + "refresh_interval": 3.5, "brightness": 80, "orientation": 0, "background_image_fit": "cover", @@ -33,8 +32,8 @@ "height": 0, "content_scale_factor": 0.98, "sensor_slot_1": "cpu", - "sensor_slot_2": "liquid", - "sensor_slot_3": "gpu" + "sensor_slot_2": "gpu", + "sensor_slot_3": "liquid" }, "layout": { @@ -94,7 +93,7 @@ "threshold_1": 30.0, "threshold_2": 35.0, "threshold_3": 40.0, - "max_scale": 55.0, + "max_scale": 45.0, "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, @@ -107,6 +106,8 @@ "positioning": { "degree_spacing": 16, "label_offset_x": 0, - "label_offset_y": 0 + "label_offset_y": 0, + "margin_top": 0, + "margin_bottom": 0 } } diff --git a/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js b/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js deleted file mode 100644 index 93365c7..0000000 --- a/etc/coolercontrol/plugins/coolerdash/ui/cc-plugin-lib.js +++ /dev/null @@ -1,259 +0,0 @@ -/* - * CoolerControl - monitor and control your cooling and other devices - * Copyright (c) 2021-2025 Guy Boldon and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -//! This is a library for CoolerControl plugins -//! It provides a set of functions to interact with the CoolerControl app -//! It is provided by the daemon for all plugins, and just needs it linked in the plugin's HTML file -//! i.e.: - -// Internal Variables -///////////////////////////////////////////////////////////// -let _messageCount = 0 -let _successfulConfigSaveCallback = () => {} -let _pluginConfig = {} -const _modes = [] -const _alerts = [] -const _profiles = [] -const _functions = [] -const _devices = [] -let _status = new Map() - -const _increaseMessageCount = () => _messageCount++ -const _decreaseMessageCount = () => { - if (_messageCount > 0) { - _messageCount-- - } - // dispatch event asynchronously - setTimeout(() => window.dispatchEvent(new CustomEvent('decreasedMessageCount')), 0) -} - -// Utility Functions -///////////////////////////////////////////////////////////// -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) -const waitTillAllMessagesReceived = async () => { - while (_messageCount !== 0) { - await new Promise((resolve) => - window.addEventListener('decreasedMessageCount', resolve, { once: true }), - ) - } -} - -// Message Processing Functions -///////////////////////////////////////////////////////////// -const _processMessages = (messageEvent) => { - // console.log("iframe message received:", event); - // The parent window messages do send an origin, even in a sandbox, so we only accept and send messages to same-origin - if (!messageEvent.isTrusted || messageEvent.origin !== document.location.origin) { - console.debug('Received invalid message', messageEvent) - return - } - if (messageEvent.data == null) { - console.log('message event data is null', messageEvent) - return - } - switch (messageEvent.data.type) { - case 'style': - if (messageEvent.data.body == null) break - const linkEl = document.createElement('link') - linkEl.rel = 'stylesheet' - linkEl.href = messageEvent.data.body - document.head.appendChild(linkEl) - break - case 'customStyle': - if (messageEvent.data.body == null) break - Object.entries(messageEvent.data.body).forEach(([key, value]) => - document.documentElement.style.setProperty(key, value), - ) - break - case 'config': - if (messageEvent.data.body == null) break - _pluginConfig = messageEvent.data.body - break - case 'configSaved': - if (messageEvent.data.body == null) { - console.warn('Plugin config was not successfully saved') - break - } - console.debug('Plugin config successfully saved', messageEvent.data.body) - _pluginConfig = messageEvent.data.body - _successfulConfigSaveCallback() - break - case 'modes': - if (messageEvent.data.body == null) break - _modes.length = 0 - _modes.push(...messageEvent.data.body) - break - case 'alerts': - if (messageEvent.data.body == null) break - _alerts.length = 0 - _alerts.push(...messageEvent.data.body) - break - case 'profiles': - if (messageEvent.data.body == null) break - _profiles.length = 0 - _profiles.push(...messageEvent.data.body) - break - case 'functions': - if (messageEvent.data.body == null) break - _functions.length = 0 - _functions.push(...messageEvent.data.body) - break - case 'devices': - if (messageEvent.data.body == null) break - _devices.length = 0 - _devices.push(...messageEvent.data.body) - break - case 'status': - if (messageEvent.data.body == null) break - _status = messageEvent.data.body - break - default: - console.log('Unknown message type', messageEvent) - } - _decreaseMessageCount() -} - -// Core Plugin Functions -///////////////////////////////////////////////////////////// - -/* Load CC parent Styles into the iframe asynchronously */ -const loadParentStyles = () => { - _increaseMessageCount() - window.parent.postMessage({ type: 'style' }, document.location.origin) - _increaseMessageCount() - window.parent.postMessage({ type: 'customStyle' }, document.location.origin) -} - -/* A convenience function for simulating a cached synchronous Plugin Config request */ -const getPluginConfig = async (force = false) => { - if (_pluginConfig.length > 0 && !force) return _pluginConfig - _increaseMessageCount() - window.parent.postMessage({ type: 'loadConfig' }, document.location.origin) - await waitTillAllMessagesReceived() - return _pluginConfig -} - -/* Convert a FormData object to a JavaScript object. Also supports multi-value fields */ -const convertFormToObject = (formData) => { - const object = {} - formData.forEach((value, key) => { - if (!Reflect.has(object, key)) { - object[key] = value - return - } - if (!Array.isArray(object[key])) { - object[key] = [object[key]] - } - object[key].push(value) - }) - return object -} - -/* A convenience function for saving the plugin config. Note: the pluginConfig must be a JSON Object */ -const savePluginConfig = async (pluginConfig) => { - _increaseMessageCount() - window.parent.postMessage({ type: 'saveConfig', body: pluginConfig }, document.location.origin) - await waitTillAllMessagesReceived() -} - -const successfulConfigSaveCallback = async (callback) => { - _successfulConfigSaveCallback = callback -} - -/* Close the plugin modal. This will end the plugin session. */ -const close = () => { - window.parent.postMessage({ type: 'close' }, document.location.origin) -} - -/* Restart the daemon & UI. This has the effect of applying any plugin changes to service configs. */ -const restart = () => { - window.parent.postMessage({ type: 'restart' }, document.location.origin) -} - -// Data Exchange Functions -///////////////////////////////////////////////////////////// - -/* A convenience function for simulating a cached synchronous Modes request */ -const getModes = async (force = false) => { - if (_modes.length > 0 && !force) return _modes - _increaseMessageCount() - window.parent.postMessage({ type: 'modes' }, document.location.origin) - await waitTillAllMessagesReceived() - return _modes -} - -/* A convenience function for simulating a cached synchronous Alerts request */ -const getAlerts = async (force = false) => { - if (_alerts.length > 0 && !force) return _alerts - _increaseMessageCount() - window.parent.postMessage({ type: 'alerts' }, document.location.origin) - await waitTillAllMessagesReceived() - return _alerts -} - -/* A convenience function for simulating a cached synchronous Profiles request */ -const getProfiles = async (force = false) => { - if (_profiles.length > 0 && !force) return _profiles - _increaseMessageCount() - window.parent.postMessage({ type: 'profiles' }, document.location.origin) - await waitTillAllMessagesReceived() - return _profiles -} - -/* A convenience function for simulating a cached synchronous Functions request */ -const getFunctions = async (force = false) => { - if (_functions.length > 0 && !force) return _functions - _increaseMessageCount() - window.parent.postMessage({ type: 'functions' }, document.location.origin) - await waitTillAllMessagesReceived() - return _functions -} - -/* A convenience function for simulating a cached synchronous Devices request */ -const getDevices = async (force = false) => { - if (_devices.length > 0 && !force) return _devices - _increaseMessageCount() - window.parent.postMessage({ type: 'devices' }, document.location.origin) - await waitTillAllMessagesReceived() - return _devices -} - -/* A convenience function for simulating a cached synchronous Status request */ -const getStatus = async (force = false) => { - if (_status.length > 0 && !force) return _status - _increaseMessageCount() - window.parent.postMessage({ type: 'status' }, document.location.origin) - await waitTillAllMessagesReceived() - return _status -} - -// Plugin Running Functions -///////////////////////////////////////////////////////////// - -/* A convenience function for running the plugin's JavaScript in an async wrapper. */ -const runPluginScript = (mainPluginFunction, loadParentStyle = true) => { - ;(async () => { - if (loadParentStyle) { - loadParentStyles() - await waitTillAllMessagesReceived() - } - await mainPluginFunction() - })() -} - -window.addEventListener('message', _processMessages) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index c508bb5..4264cda 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -4,7 +4,7 @@ CoolerDash Settings - +