diff --git a/Build/README.md b/Build/README.md
new file mode 100644
index 0000000..52e4533
--- /dev/null
+++ b/Build/README.md
@@ -0,0 +1,100 @@
+# Ryzen Master Commander – Installation
+
+Linux GUI application (PyQt6) to monitor and control Ryzen laptops: temperature, fan (NBFC), TDP (ryzenadj), real-time graphs, and custom profiles.
+
+**Target:** Ubuntu 24.04.4 (Noble). May work on other Ubuntu/Debian-based systems.
+
+**Requirements:** Internet connection (the installer will fetch all dependencies).
+
+---
+
+## Contents of this folder
+
+| File | Description |
+|------|-------------|
+| **install-standalone.sh** | One-shot installer: system deps, extract bundle, Python venv, polkit, desktop shortcut, ryzenadj, nbfc. |
+| **RyzenMasterCommander-bundle.tar.gz** | Application sources (no installer inside). |
+| **README.md** | This file. |
+
+The installer script must sit in the **same folder** as the `.tar.gz` file.
+
+---
+
+## Dependencies
+
+The script installs everything; you do not need to install anything manually.
+
+### System (apt)
+
+- **python3** – Python 3 interpreter
+- **python3-venv** – Virtual environments
+- **python3.X-venv** – Venv for default Python (e.g. python3.12-venv on Ubuntu 24.04)
+- **python3-pip** – Pip for Python packages
+- **libxcb-cursor0** – Qt6 xcb plugin (GUI)
+- **lm-sensors** – Power/temperature readout (`sensors` command)
+- **curl** – Used only if needed to download nbfc .deb
+
+### Optional (installed by the script when available)
+
+- **ryzenadj** – TDP/power limits (installed via Snap: `ryzenadj --beta --devmode`)
+- **nbfc** – Fan control (apt if in repos, otherwise .deb from [nbfc-linux](https://github.com/nbfc-linux/nbfc-linux/releases))
+
+### Python (pip, inside venv)
+
+- **PyQt6** – GUI
+- **pyqtgraph** – Graphs
+- **numpy** – Numeric data
+- **Pillow** – Images
+- **pystray** – System tray icon
+
+---
+
+## Installation (single command)
+
+1. Download or clone this repo and go into the **Build** folder (or download only the Build folder with the three files).
+2. In a terminal, inside that folder:
+
+```bash
+chmod +x install-standalone.sh
+./install-standalone.sh
+```
+
+You will be prompted for your sudo password to install system packages, polkit, and optionally Snap/nbfc.
+
+The script will:
+
+1. Install system dependencies (Python, venv, pip, libxcb-cursor0).
+2. Extract `RyzenMasterCommander-bundle.tar.gz` to `~/.local/ryzen-master-commander`.
+3. Create the run script and Python virtual environment and install Python dependencies.
+4. Install the polkit policy (fewer password prompts for TDP/fan).
+5. Create a desktop shortcut.
+6. Install **ryzenadj** via Snap (if Snap is available).
+7. Install **nbfc** via apt or by downloading the .deb from nbfc-linux releases.
+
+Then run **Ryzen Master Commander** from the desktop icon or:
+
+```bash
+~/.local/ryzen-master-commander/run-ryzen-master-commander.sh
+```
+
+### System-wide install (/opt)
+
+```bash
+./install-standalone.sh --system
+```
+
+Uses sudo to install under `/opt/ryzen-master-commander`.
+
+---
+
+## After installation
+
+- **TDP (ryzenadj):** Installed by the script via Snap when available. If not, run:
+ `sudo snap install ryzenadj --beta --devmode`
+- **Fan (nbfc):** Installed by the script (apt or .deb). Configure a profile for your laptop (e.g. “Lenovo ThinkPad T14 Gen2” for Lenovo V15 G3).
+
+---
+
+## License
+
+This project is licensed under the Apache-2.0 license. Ryzen Master Commander uses [nbfc-linux](https://github.com/nbfc-linux/nbfc-linux) and [ryzenadj](https://github.com/FlyGoat/RyzenAdj).
diff --git a/Build/RyzenMasterCommander-bundle.tar.gz b/Build/RyzenMasterCommander-bundle.tar.gz
new file mode 100644
index 0000000..c37ce41
Binary files /dev/null and b/Build/RyzenMasterCommander-bundle.tar.gz differ
diff --git a/Build/create-bundle.sh b/Build/create-bundle.sh
new file mode 100755
index 0000000..1d60dc2
--- /dev/null
+++ b/Build/create-bundle.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# Genera RyzenMasterCommander-bundle.tar.gz con solo los fuentes (sin Build/, sin .git, sin .venv).
+# El instalador install-standalone.sh debe estar FUERA de este comprimido; se deja en Build/ para copiarlo aparte.
+# Uso: desde la raíz del repositorio: ./Build/create-bundle.sh
+# Resultado: RyzenMasterCommander-bundle.tar.gz en la raíz del repositorio.
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+OUTPUT="$SCRIPT_DIR/RyzenMasterCommander-bundle.tar.gz"
+
+cd "$REPO_ROOT"
+echo "Creando comprimido (solo fuentes, sin Build/)..."
+tar czf "$OUTPUT" \
+ src bin polkit share tdp_profiles img \
+ requirements.txt version.txt
+
+echo "Creado: $OUTPUT"
+echo ""
+echo "Para distribuir: copia la carpeta Build/ completa al Ubuntu; dentro estarán"
+echo " install-standalone.sh y RyzenMasterCommander-bundle.tar.gz."
+echo " Luego: cd Build && chmod +x install-standalone.sh && ./install-standalone.sh"
+echo ""
diff --git a/Build/install-standalone.sh b/Build/install-standalone.sh
new file mode 100755
index 0000000..cc7c370
--- /dev/null
+++ b/Build/install-standalone.sh
@@ -0,0 +1,167 @@
+#!/usr/bin/env bash
+# Instalador independiente: debe estar FUERA del comprimido, en la misma carpeta que RyzenMasterCommander-bundle.tar.gz
+# Uso: copia este script y RyzenMasterCommander-bundle.tar.gz a la misma carpeta; ejecuta: ./install-standalone.sh
+# Opción: ./install-standalone.sh --system (instala en /opt)
+
+set -e
+
+# El script debe estar en la misma carpeta que RyzenMasterCommander-bundle.tar.gz (p. ej. dentro de Build/)
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+BUNDLE="$SCRIPT_DIR/RyzenMasterCommander-bundle.tar.gz"
+INSTALL_SYSTEM=false
+if [ "${1:-}" = "--system" ]; then
+ INSTALL_SYSTEM=true
+fi
+
+if [ "$INSTALL_SYSTEM" = true ]; then
+ INSTALL_DIR="/opt/ryzen-master-commander"
+ echo "Modo sistema: se instalará en $INSTALL_DIR"
+else
+ INSTALL_DIR="${HOME}/.local/ryzen-master-commander"
+ echo "Modo usuario: se instalará en $INSTALL_DIR"
+fi
+
+if [ ! -f "$BUNDLE" ]; then
+ echo "Error: no se encuentra el comprimido."
+ echo " Coloca RyzenMasterCommander-bundle.tar.gz en la misma carpeta que este script:"
+ echo " $SCRIPT_DIR"
+ exit 1
+fi
+
+echo "Comprimido: $BUNDLE"
+echo "Destino: $INSTALL_DIR"
+echo ""
+
+# [1/8] Instalar todas las dependencias de sistema (el usuario no tiene que instalar nada a mano)
+echo "[1/8] Instalando dependencias de sistema (se pedirá contraseña sudo)..."
+sudo apt-get update -qq
+sudo apt-get install -y python3 python3-venv python3-pip libxcb-cursor0 lm-sensors
+# En Ubuntu 24.04+ el venv necesita python3.X-venv (p. ej. python3.12-venv) para ensurepip
+PYVER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)"
+if [ -n "$PYVER" ]; then
+ sudo apt-get install -y "python${PYVER}-venv"
+fi
+
+# [2/8] Descomprimir el .tar.gz en el directorio de instalación
+echo "[2/8] Descomprimiendo RyzenMasterCommander-bundle.tar.gz en $INSTALL_DIR..."
+if [ "$INSTALL_SYSTEM" = true ]; then
+ sudo mkdir -p "$INSTALL_DIR"
+ sudo tar xzf "$BUNDLE" -C "$INSTALL_DIR"
+ sudo chown -R "$USER:$USER" "$INSTALL_DIR"
+else
+ mkdir -p "$INSTALL_DIR"
+ tar xzf "$BUNDLE" -C "$INSTALL_DIR"
+fi
+
+# [3/8] Script de ejecución
+echo "[3/8] Configurando script de ejecución..."
+cat > "$INSTALL_DIR/run-ryzen-master-commander.sh" << 'RUNSCRIPT'
+#!/usr/bin/env bash
+set -e
+cd "$(dirname "$0")"
+if [ ! -d ".venv" ]; then
+ python3 -m venv .venv
+ .venv/bin/pip install -r requirements.txt
+fi
+export PYTHONPATH="$(pwd):${PYTHONPATH}"
+exec "$(pwd)/.venv/bin/python" -m src.main
+RUNSCRIPT
+chmod +x "$INSTALL_DIR/run-ryzen-master-commander.sh"
+
+# [4/8] Entorno virtual y dependencias Python
+echo "[4/8] Creando entorno virtual e instalando dependencias Python..."
+cd "$INSTALL_DIR"
+python3 -m venv .venv
+.venv/bin/pip install -q -r requirements.txt
+
+# [5/8] Polkit
+echo "[5/8] Instalando política polkit..."
+POLKIT_SRC="$INSTALL_DIR/polkit/com.merrythieves.ryzenadj.policy"
+POLKIT_DST="/usr/share/polkit-1/actions/com.merrythieves.ryzenadj.policy"
+if [ -f "$POLKIT_SRC" ]; then
+ sudo cp "$POLKIT_SRC" "$POLKIT_DST"
+ sudo chown root:root "$POLKIT_DST"
+ sudo chmod 0644 "$POLKIT_DST"
+ echo " Política instalada. Opcional: sudo systemctl daemon-reload && sudo systemctl restart polkit"
+else
+ echo " No se encontró política; omitiendo polkit."
+fi
+
+# [6/8] Acceso directo en el escritorio
+echo "[6/8] Creando acceso directo en el escritorio..."
+DESKTOP_DIR=""
+[ -d "$HOME/Escritorio" ] && DESKTOP_DIR="$HOME/Escritorio"
+[ -d "$HOME/Desktop" ] && DESKTOP_DIR="${DESKTOP_DIR:-$HOME/Desktop}"
+ICON="$INSTALL_DIR/img/icon.png"
+[ ! -f "$ICON" ] && ICON=""
+if [ -n "$DESKTOP_DIR" ]; then
+ SHORTCUT="$DESKTOP_DIR/RyzenMasterCommander.desktop"
+ cat > "$SHORTCUT" << DESKTOP
+[Desktop Entry]
+Type=Application
+Name=Ryzen Master Commander
+Comment=Monitorizar y controlar TDP y ventilador Ryzen
+Exec=$INSTALL_DIR/run-ryzen-master-commander.sh
+Icon=$ICON
+Terminal=false
+Categories=Utility;System;
+DESKTOP
+ chmod +x "$SHORTCUT"
+ echo " Creado: $SHORTCUT"
+else
+ echo " No se encontró Escritorio ni Desktop; no se creó acceso directo."
+fi
+
+# [7/8] ryzenadj (control TDP)
+echo "[7/8] Instalando ryzenadj (control TDP)..."
+if command -v snap &>/dev/null; then
+ if ! snap list ryzenadj &>/dev/null 2>&1; then
+ sudo snap install ryzenadj --beta --devmode 2>/dev/null && echo " ryzenadj instalado (snap)." || echo " ryzenadj: no se pudo instalar por snap (instálalo manualmente: sudo snap install ryzenadj --beta --devmode)."
+ else
+ echo " ryzenadj ya está instalado."
+ fi
+else
+ echo " Snap no está instalado; para control TDP instala ryzenadj manualmente (p. ej. desde fuentes)."
+fi
+
+# [8/8] nbfc (control ventilador) y arranque del servicio
+echo "[8/8] Instalando nbfc (control ventilador)..."
+if command -v nbfc &>/dev/null; then
+ echo " nbfc ya está instalado."
+else
+ if sudo apt-get install -y nbfc &>/dev/null 2>&1; then
+ echo " nbfc instalado desde los repos."
+ else
+ NBFC_DEB_URL="https://github.com/nbfc-linux/nbfc-linux/releases/download/0.3.19/nbfc-linux_0.3.19_amd64.deb"
+ if command -v curl &>/dev/null; then
+ if curl -sLf "$NBFC_DEB_URL" -o /tmp/nbfc-linux.deb 2>/dev/null; then
+ sudo dpkg -i /tmp/nbfc-linux.deb 2>/dev/null && sudo apt-get install -f -y -qq 2>/dev/null && echo " nbfc instalado desde nbfc-linux (GitHub)." || echo " nbfc: no se pudo instalar el .deb."
+ rm -f /tmp/nbfc-linux.deb
+ else
+ echo " nbfc: descarga fallida; instálalo desde https://github.com/nbfc-linux/nbfc-linux/releases"
+ fi
+ else
+ sudo apt-get install -y curl -qq 2>/dev/null || true
+ if curl -sLf "$NBFC_DEB_URL" -o /tmp/nbfc-linux.deb 2>/dev/null; then
+ sudo dpkg -i /tmp/nbfc-linux.deb 2>/dev/null && sudo apt-get install -f -y -qq 2>/dev/null && echo " nbfc instalado desde nbfc-linux (GitHub)." || echo " nbfc: no se pudo instalar el .deb."
+ rm -f /tmp/nbfc-linux.deb
+ else
+ echo " nbfc: instálalo desde https://github.com/nbfc-linux/nbfc-linux/releases"
+ fi
+ fi
+ fi
+fi
+# Actualizar configs e intentar arrancar el servicio nbfc
+if command -v nbfc &>/dev/null; then
+ sudo nbfc update 2>/dev/null || true
+ sudo systemctl enable nbfc_service 2>/dev/null || true
+ (sudo nbfc config -s auto 2>/dev/null; sudo nbfc start 2>/dev/null) || true
+ echo " Servicio nbfc: enable al arranque; si no arranca, elige un perfil en la app (p. ej. Lenovo ThinkPad T14 Gen2)."
+fi
+
+echo ""
+echo "=== Instalación completada ==="
+echo " Ejecutar la aplicación:"
+echo " $INSTALL_DIR/run-ryzen-master-commander.sh"
+echo " o usar el icono del escritorio."
+echo ""
diff --git a/Build/install.sh b/Build/install.sh
new file mode 100755
index 0000000..c1e4ea1
--- /dev/null
+++ b/Build/install.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+# Instalador todo-en-uno para Ryzen Master Commander.
+# Uso: desde el repositorio clonado, ejecutar: ./Build/install.sh
+# Opción: ./Build/install.sh --system (instala en /opt, requiere sudo para los archivos)
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+INSTALL_SYSTEM=false
+if [ "${1:-}" = "--system" ]; then
+ INSTALL_SYSTEM=true
+fi
+
+# Comprobar que estamos en el repositorio completo (no solo la carpeta Build)
+if [ ! -f "$REPO_ROOT/requirements.txt" ] || [ ! -d "$REPO_ROOT/src" ]; then
+ echo "Error: hace falta el repositorio completo."
+ echo " Debe existir $REPO_ROOT/requirements.txt y $REPO_ROOT/src/"
+ echo " Copia o clona la carpeta completa del proyecto (no solo Build/) y ejecuta: ./Build/install.sh"
+ exit 1
+fi
+
+if [ "$INSTALL_SYSTEM" = true ]; then
+ INSTALL_DIR="/opt/ryzen-master-commander"
+ echo "Modo sistema: se instalará en $INSTALL_DIR (se usará sudo para copiar archivos)."
+else
+ INSTALL_DIR="${HOME}/.local/ryzen-master-commander"
+ echo "Modo usuario: se instalará en $INSTALL_DIR"
+fi
+
+echo "Origen (repositorio): $REPO_ROOT"
+echo "Destino: $INSTALL_DIR"
+echo ""
+
+# Dependencias de sistema (Python, venv, pip, libs Qt para la GUI)
+echo "[1/6] Comprobando dependencias de sistema..."
+if ! command -v python3 &>/dev/null; then
+ echo "Se necesita python3. Instálalo con: sudo apt update && sudo apt install -y python3 python3-venv python3-pip"
+ exit 1
+fi
+# Instalar paquetes necesarios (en Ubuntu recién instalado suelen faltar)
+NEEDED=""
+for pkg in python3-venv python3-pip libxcb-cursor0; do
+ if ! dpkg -l "$pkg" &>/dev/null 2>&1; then
+ NEEDED="$NEEDED $pkg"
+ fi
+done
+if [ -n "$NEEDED" ]; then
+ echo "Instalando:$NEEDED"
+ sudo apt-get update -qq
+ sudo apt-get install -y $NEEDED
+fi
+
+# Crear directorio de instalación y copiar fuentes necesarios
+echo "[2/6] Copiando fuentes a $INSTALL_DIR..."
+if [ "$INSTALL_SYSTEM" = true ]; then
+ sudo mkdir -p "$INSTALL_DIR"
+ sudo cp -r "$REPO_ROOT/src" "$REPO_ROOT/bin" "$REPO_ROOT/polkit" "$REPO_ROOT/share" "$REPO_ROOT/tdp_profiles" "$REPO_ROOT/img" "$INSTALL_DIR/"
+ sudo cp "$REPO_ROOT/requirements.txt" "$REPO_ROOT/version.txt" "$INSTALL_DIR/"
+ sudo chown -R "$USER:$USER" "$INSTALL_DIR"
+else
+ mkdir -p "$INSTALL_DIR"
+ cp -r "$REPO_ROOT/src" "$REPO_ROOT/bin" "$REPO_ROOT/polkit" "$REPO_ROOT/share" "$REPO_ROOT/tdp_profiles" "$REPO_ROOT/img" "$INSTALL_DIR/"
+ cp "$REPO_ROOT/requirements.txt" "$REPO_ROOT/version.txt" "$INSTALL_DIR/"
+fi
+
+# Script de ejecución en el directorio instalado
+echo "[3/6] Configurando script de ejecución..."
+cat > "$INSTALL_DIR/run-ryzen-master-commander.sh" << 'RUNSCRIPT'
+#!/usr/bin/env bash
+set -e
+cd "$(dirname "$0")"
+if [ ! -d ".venv" ]; then
+ python3 -m venv .venv
+ .venv/bin/pip install -r requirements.txt
+fi
+export PYTHONPATH="$(pwd):${PYTHONPATH}"
+exec "$(pwd)/.venv/bin/python" -m src.main
+RUNSCRIPT
+chmod +x "$INSTALL_DIR/run-ryzen-master-commander.sh"
+
+# Entorno virtual e dependencias Python
+echo "[4/6] Creando entorno virtual e instalando dependencias Python..."
+cd "$INSTALL_DIR"
+python3 -m venv .venv
+.venv/bin/pip install -q -r requirements.txt
+
+# Polkit (ryzenadj/nbfc sin pedir contraseña cada vez)
+echo "[5/6] Instalando política polkit..."
+POLKIT_SRC="$INSTALL_DIR/polkit/com.merrythieves.ryzenadj.policy"
+POLKIT_DST="/usr/share/polkit-1/actions/com.merrythieves.ryzenadj.policy"
+if [ -f "$POLKIT_SRC" ]; then
+ sudo cp "$POLKIT_SRC" "$POLKIT_DST"
+ sudo chown root:root "$POLKIT_DST"
+ sudo chmod 0644 "$POLKIT_DST"
+ echo " Política instalada. Opcional: sudo systemctl daemon-reload && sudo systemctl restart polkit"
+else
+ echo " No se encontró $POLKIT_SRC; omitiendo polkit."
+fi
+
+# Acceso directo en el escritorio
+echo "[6/6] Creando acceso directo en el escritorio..."
+DESKTOP_DIR=""
+[ -d "$HOME/Escritorio" ] && DESKTOP_DIR="$HOME/Escritorio"
+[ -d "$HOME/Desktop" ] && DESKTOP_DIR="${DESKTOP_DIR:-$HOME/Desktop}"
+ICON="$INSTALL_DIR/img/icon.png"
+[ ! -f "$ICON" ] && ICON=""
+if [ -n "$DESKTOP_DIR" ]; then
+ SHORTCUT="$DESKTOP_DIR/RyzenMasterCommander.desktop"
+ cat > "$SHORTCUT" << DESKTOP
+[Desktop Entry]
+Type=Application
+Name=Ryzen Master Commander
+Comment=Monitorizar y controlar TDP y ventilador Ryzen
+Exec=$INSTALL_DIR/run-ryzen-master-commander.sh
+Icon=$ICON
+Terminal=false
+Categories=Utility;System;
+DESKTOP
+ chmod +x "$SHORTCUT"
+ echo " Creado: $SHORTCUT"
+else
+ echo " No se encontró Escritorio ni Desktop; no se creó acceso directo."
+fi
+
+echo ""
+echo "=== Instalación completada ==="
+echo " Ejecutar la aplicación:"
+echo " $INSTALL_DIR/run-ryzen-master-commander.sh"
+echo ""
+echo " Dependencias opcionales (control TDP y ventilador):"
+echo " - ryzenadj: sudo snap install ryzenadj --beta --devmode"
+echo " - nbfc: según tu distro (p. ej. AUR, o desde nbfc-linux)"
+echo ""
diff --git a/Makefile b/Makefile
index a4a756a..6143d67 100644
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,7 @@ PACKAGE_NAME := ryzen-master-commander
RPM_BUILD_DIR := $(HOME)/rpmbuild
# Default target
+.NOTPARALLEL: all
all: arch deb rpm flatpak
# Create build directory
diff --git a/bin/ryzen-master-commander-helper b/bin/ryzen-master-commander-helper
index e30099c..9493d77 100644
--- a/bin/ryzen-master-commander-helper
+++ b/bin/ryzen-master-commander-helper
@@ -6,7 +6,7 @@ shift
case "$command" in
"ryzenadj")
- pkexec /usr/bin/ryzenadj "$@"
+ pkexec /snap/bin/ryzenadj "$@"
;;
"nbfc")
pkexec /usr/bin/nbfc "$@"
diff --git a/install-polkit-rmc.sh b/install-polkit-rmc.sh
new file mode 100755
index 0000000..3dcdaec
--- /dev/null
+++ b/install-polkit-rmc.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -e
+
+echo "Instalando políticas de polkit para Ryzen Master Commander..."
+
+if [ "$EUID" -ne 0 ]; then
+ echo "Se requiere sudo para copiar la política a /usr/share/polkit-1/actions/"
+ exec sudo "$0" "$@"
+fi
+
+PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
+POLKIT_SRC="$PROJECT_DIR/polkit/com.merrythieves.ryzenadj.policy"
+POLKIT_DST="/usr/share/polkit-1/actions/com.merrythieves.ryzenadj.policy"
+
+if [ ! -f "$POLKIT_SRC" ]; then
+ echo "No se encuentra el archivo de política en: $POLKIT_SRC"
+ exit 1
+fi
+
+cp "$POLKIT_SRC" "$POLKIT_DST"
+chown root:root "$POLKIT_DST"
+chmod 0644 "$POLKIT_DST"
+
+echo "Política instalada en $POLKIT_DST"
+echo "Es posible que tengas que reiniciar la sesión o ejecutar:"
+echo " sudo systemctl restart polkit"
+echo "para que los cambios se apliquen."
+
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
index 633c80f..5235682 100644
--- a/packaging/arch/PKGBUILD
+++ b/packaging/arch/PKGBUILD
@@ -3,7 +3,7 @@
pkgname=ryzen-master-commander
_realname=Ryzen-Master-Commander
-pkgver=1.0.13
+pkgver=1.2.0
pkgrel=1
pkgdesc="TDP and fan control for AMD Ryzen processors"
arch=('any')
@@ -20,11 +20,11 @@ build() {
mkdir -p "$srcdir/$pkgname-$pkgver"
tar -xzf "$srcdir/$pkgname-$pkgver.tar.gz" -C "$srcdir/$pkgname-$pkgver" --strip-components=1
cd "$srcdir/$pkgname-$pkgver"
-
- python -m build --wheel --no-isolation
+
+ /usr/bin/python -m build --wheel --no-isolation
}
package() {
cd "$srcdir/$pkgname-$pkgver"
- python -m installer --destdir="$pkgdir" dist/*.whl
+ /usr/bin/python -m installer --destdir="$pkgdir" dist/*.whl
}
\ No newline at end of file
diff --git a/polkit/com.merrythieves.ryzenadj.policy b/polkit/com.merrythieves.ryzenadj.policy
index bf288b5..cf09e08 100644
--- a/polkit/com.merrythieves.ryzenadj.policy
+++ b/polkit/com.merrythieves.ryzenadj.policy
@@ -11,11 +11,11 @@
Authentication is required to change processor settings
cpu
- auth_admin_keep
- auth_admin_keep
+ yes
+ yes
yes
- /usr/bin/ryzenadj
+ /snap/bin/ryzenadj
true
@@ -24,8 +24,8 @@
Authentication is required to control fan settings
fan
- auth_admin_keep
- auth_admin_keep
+ yes
+ yes
yes
/usr/bin/nbfc
diff --git a/run-ryzen-master-commander.sh b/run-ryzen-master-commander.sh
new file mode 100755
index 0000000..a8fc7ac
--- /dev/null
+++ b/run-ryzen-master-commander.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+set -e
+
+# Ir a la carpeta del proyecto
+cd "$(dirname "$0")"
+
+# Asegurar que el entorno virtual existe e instala dependencias básicas si hace falta
+if [ ! -d ".venv" ]; then
+ python3 -m venv .venv
+ .venv/bin/pip install -r requirements.txt
+fi
+
+# Añadir la carpeta del proyecto al PYTHONPATH para que se encuentre el paquete src
+export PYTHONPATH="$(pwd):${PYTHONPATH}"
+
+# Ejecutar como usuario (la sesión gráfica carga bien). La contraseña se pide
+# una sola vez cuando la app use nbfc/ryzenadj (polkit/pkexec).
+# Si ves error de "xcb" o "libxcb-cursor0", instala: sudo apt install libxcb-cursor0
+exec "$(pwd)/.venv/bin/python" -m src.main
+
diff --git a/src/__init__.py b/src/__init__.py
index e9537c3..df3eb77 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1,3 +1,2 @@
from .version import __version__
-from . import main
from . import app
diff --git a/src/app/main_window.py b/src/app/main_window.py
index 70ffbd7..4f6a8b9 100644
--- a/src/app/main_window.py
+++ b/src/app/main_window.py
@@ -53,8 +53,11 @@ def __init__(self):
# Set up system tray
self.setup_system_tray()
- # Set auto control by default
- self.radio_auto_control.setChecked(True)
+ # Set auto control by default only if nbfc is running; otherwise manual to avoid errors
+ if self.check_nbfc_running():
+ self.radio_auto_control.setChecked(True)
+ else:
+ self.radio_manual_control.setChecked(True)
self.update_fan_control_visibility()
# Start reading system values
@@ -71,9 +74,8 @@ def check_nbfc_running(self):
result = subprocess.run(
["nbfc", "status"], capture_output=True, text=True
)
- return "ERROR: connect()" not in result.stderr
+ return "ERROR: connect()" not in (result.stderr or "")
except Exception:
- print(f"nbfc not running: {result.stderr}")
return False
def start_nbfc_service(self):
@@ -125,6 +127,14 @@ def setup_system_tray(self):
tray_menu.addAction(toggle_auto_action)
self.toggle_auto_action = toggle_auto_action
+ tray_menu.addSeparator()
+ tdp_15_action = QAction("15W", self)
+ tdp_15_action.triggered.connect(lambda: self.profile_manager.apply_preset_tdp(15))
+ tray_menu.addAction(tdp_15_action)
+ tdp_25_action = QAction("25W", self)
+ tdp_25_action.triggered.connect(lambda: self.profile_manager.apply_preset_tdp(25))
+ tray_menu.addAction(tdp_25_action)
+
tray_menu.addSeparator()
quit_action = QAction("Quit", self)
@@ -431,7 +441,6 @@ def on_finished(exit_code, exit_status):
def set_auto_control(self):
if self.radio_auto_control.isChecked():
- # Create QProcess for non-blocking execution
process = QProcess(self)
def on_finished(exit_code, exit_status):
@@ -439,11 +448,13 @@ def on_finished(exit_code, exit_status):
print("Auto fan control enabled")
else:
stderr = process.readAllStandardError().data().decode('utf-8', errors='ignore')
- error_msg = f"Error setting automatic fan control (exit code: {exit_code})"
- if stderr:
- error_msg += f": {stderr}"
- print(error_msg)
- # Remove from active processes list
+ if not getattr(self, "_auto_fan_error_shown", False):
+ self._auto_fan_error_shown = True
+ msg = "Automatic fan control unavailable (NBFC service not running or not configured)."
+ if stderr and "Service not running" in stderr:
+ print(msg)
+ else:
+ print(f"{msg} {stderr.strip() or exit_code}")
if process in self.active_processes:
self.active_processes.remove(process)
diff --git a/src/app/nbfc_manager.py b/src/app/nbfc_manager.py
index f288750..bc04013 100644
--- a/src/app/nbfc_manager.py
+++ b/src/app/nbfc_manager.py
@@ -42,7 +42,9 @@ def is_nbfc_configured():
try:
# First check if the service can start
result = subprocess.run(
- ["sudo", "nbfc", "start"], capture_output=True, text=True
+ ["bin/ryzen-master-commander-helper", "nbfc", "start"],
+ capture_output=True,
+ text=True,
)
# If there's an error about missing config file, it's not configured
return (
@@ -63,11 +65,16 @@ def update_nbfc_configs(parent=None, callback=None):
"""
process = QProcess(parent)
- def on_finished(exit_code, exit_status):
+ def on_finished(
+ exit_code,
+ exit_status,
+ callback_fn=callback,
+ process_obj=process,
+ ):
success = exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit
- if callback:
- callback(success)
- process.deleteLater()
+ if callback_fn:
+ callback_fn(success)
+ process_obj.deleteLater()
process.finished.connect(on_finished)
process.start("pkexec", ["nbfc", "update"])
@@ -137,12 +144,23 @@ def set_nbfc_config(config_name, parent=None, callback=None):
"""
process = QProcess(parent)
- def on_finished(exit_code, exit_status):
- stderr = process.readAllStandardError().data().decode('utf-8', errors='ignore')
- success = exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit and "ERROR" not in stderr
- if callback:
- callback(success)
- process.deleteLater()
+ def on_finished(
+ exit_code,
+ exit_status,
+ callback_fn=callback,
+ process_obj=process,
+ ):
+ stderr = process_obj.readAllStandardError().data().decode(
+ "utf-8", errors="ignore"
+ )
+ success = (
+ exit_code == 0
+ and exit_status == QProcess.ExitStatus.NormalExit
+ and "ERROR" not in stderr
+ )
+ if callback_fn:
+ callback_fn(success)
+ process_obj.deleteLater()
process.finished.connect(on_finished)
process.start("pkexec", ["nbfc", "config", "-s", config_name])
@@ -159,12 +177,17 @@ def start_nbfc_service(parent=None, callback=None):
"""
process = QProcess(parent)
- def on_finished(exit_code, exit_status):
+ def on_finished(
+ exit_code,
+ exit_status,
+ callback_fn=callback,
+ process_obj=process,
+ ):
# Check if service is actually running after the command
success = NBFCManager.is_nbfc_running()
- if callback:
- callback(success)
- process.deleteLater()
+ if callback_fn:
+ callback_fn(success)
+ process_obj.deleteLater()
process.finished.connect(on_finished)
process.start("pkexec", ["nbfc", "start"])
@@ -193,17 +216,22 @@ def setup_nbfc(parent=None):
return True
print("NBFC service failed to start, checking configuration...")
- # If still not running, update configs silently
- NBFCManager.update_nbfc_configs()
+ # If still not running, update configs silently (async; list may already be from package)
+ NBFCManager.update_nbfc_configs(parent=parent)
# Try to get and apply recommended config automatically
recommended = NBFCManager.get_recommended_config()
if recommended:
- # Apply recommended config silently
- if NBFCManager.set_nbfc_config(recommended):
- print(f"Applied recommended config: {recommended}")
- return NBFCManager.start_nbfc_service()
+ def on_recommended_set(success):
+ if success:
+ print(f"Applied recommended config: {recommended}. Starting service...")
+ NBFCManager.start_nbfc_service(parent=parent)
+ else:
+ print("Failed to apply recommended config.")
+
+ NBFCManager.set_nbfc_config(recommended, parent=parent, callback=on_recommended_set)
+ return True
# Only show dialog if we couldn't find or apply a recommended config
print(
@@ -214,8 +242,23 @@ def setup_nbfc(parent=None):
config_dialog.exec() == QDialog.DialogCode.Accepted
and config_dialog.selected_config
):
- if NBFCManager.set_nbfc_config(config_dialog.selected_config):
- return NBFCManager.start_nbfc_service()
+ config_name = config_dialog.selected_config
+ # Chain async: set config first, then start service when config is done
+ def on_config_set(success):
+ if success:
+ print(f"Config '{config_name}' applied. Starting NBFC service...")
+ NBFCManager.start_nbfc_service(parent=parent, callback=on_service_started)
+ else:
+ print(f"Failed to apply config '{config_name}'.")
+
+ def on_service_started(success):
+ if success:
+ print("NBFC service is running.")
+ else:
+ print("NBFC service failed to start. Try running: sudo nbfc start")
+
+ NBFCManager.set_nbfc_config(config_name, parent=parent, callback=on_config_set)
+ return True # We started the async chain; service may come up shortly
return False
diff --git a/src/app/profile_manager.py b/src/app/profile_manager.py
index 21a3777..ae77a25 100644
--- a/src/app/profile_manager.py
+++ b/src/app/profile_manager.py
@@ -13,7 +13,7 @@
QWidget,
QVBoxLayout,
)
-from PyQt6.QtCore import pyqtSlot, Qt, QProcess
+from PyQt6.QtCore import pyqtSlot, Qt, QProcess, QSettings
from src.app.system_utils import apply_tdp_settings
@@ -47,6 +47,9 @@ def __init__(self):
self.current_profile = None
self.cached_profiles = self.load_profiles()
+ # Initialize settings for persisting TDP values
+ self.settings = QSettings("MerryThieves", "RyzenMasterCommander")
+
def create_widgets(self, parent):
self.parent = parent
layout = parent.layout()
@@ -78,6 +81,18 @@ def create_widgets(self, parent):
# Add the power controls to the main layout
layout.addLayout(power_controls_layout)
+
+ # Preset TDP buttons (15W and 25W)
+ preset_buttons_layout = QHBoxLayout()
+ btn_15w = QPushButton("15W")
+ btn_15w.setToolTip("Aplica 15W (boost y sostenido). Tctl 70°C.")
+ btn_15w.clicked.connect(lambda: self.apply_preset_tdp(15))
+ btn_25w = QPushButton("25W")
+ btn_25w.setToolTip("Aplica 25W (boost y sostenido). Tctl 70°C.")
+ btn_25w.clicked.connect(lambda: self.apply_preset_tdp(25))
+ preset_buttons_layout.addWidget(btn_15w)
+ preset_buttons_layout.addWidget(btn_25w)
+ layout.addLayout(preset_buttons_layout)
# Collapsible Advanced Settings Section
self.advanced_container = QWidget()
@@ -209,6 +224,8 @@ def create_widgets(self, parent):
# Populate profile dropdown
self.update_profile_dropdown()
+ # No auto-apply on startup: TDP is applied only when the user selects a profile or clicks 15W/25W
+
def toggle_advanced_section(self):
"""Toggle visibility of advanced settings section"""
if self.advanced_content.isVisible():
@@ -227,6 +244,29 @@ def toggle_profile_section(self):
self.profile_content.show()
self.profile_toggle_btn.setText("▼ Profile Management")
+ def apply_preset_tdp(self, watts):
+ """Apply a preset TDP (15 or 25W) with tctl-temp 70°C. 25W uses --max-performance."""
+ use_max_performance = watts == 25
+ profile = {
+ "name": f"Preset {watts}W",
+ "fast-limit": watts,
+ "slow-limit": watts,
+ "slow-time": 60,
+ "tctl-temp": 70,
+ "apu-skin-temp": 50,
+ "max-performance": use_max_performance,
+ "power-saving": False,
+ }
+ self.fast_limit_entry.setText(str(watts))
+ self.slow_limit_entry.setText(str(watts))
+ self.slow_time_entry.setText("60")
+ self.tctl_temp_entry.setText("70")
+ self.apu_skin_temp_entry.setText("50")
+ self.max_performance_var.setChecked(use_max_performance)
+ self.power_saving_var.setChecked(False)
+ apply_tdp_settings(profile, parent=self.parent)
+ self.save_tdp_settings(profile)
+
def auto_apply_basic_settings(self):
"""Auto-apply basic settings when values change"""
# Only auto-apply if advanced settings are hidden
@@ -239,9 +279,48 @@ def auto_apply_basic_settings(self):
}
apply_tdp_settings(basic_profile, parent=self.parent)
print("Auto-applied basic TDP settings")
+
+ # Save settings for auto-restore on next startup
+ self.save_tdp_settings(basic_profile)
except (ValueError, TypeError) as e:
print(f"Error auto-applying settings: {e}")
+ def save_tdp_settings(self, profile):
+ """Save TDP settings to persistent storage"""
+ try:
+ self.settings.setValue("tdp/fast_limit", profile.get("fast-limit"))
+ self.settings.setValue("tdp/slow_limit", profile.get("slow-limit"))
+ self.settings.sync()
+ print(f"Saved TDP settings: Fast={profile.get('fast-limit')}W, Slow={profile.get('slow-limit')}W")
+ except Exception as e:
+ print(f"Error saving TDP settings: {e}")
+
+ def restore_tdp_settings(self):
+ """Restore and apply saved TDP settings on startup"""
+ try:
+ fast_limit = self.settings.value("tdp/fast_limit", type=int)
+ slow_limit = self.settings.value("tdp/slow_limit", type=int)
+
+ if fast_limit and slow_limit:
+ # Update UI fields
+ self.fast_limit_entry.setText(str(fast_limit))
+ self.slow_limit_entry.setText(str(slow_limit))
+
+ # Apply the settings
+ basic_profile = {
+ "fast-limit": fast_limit,
+ "slow-limit": slow_limit,
+ }
+ apply_tdp_settings(basic_profile, parent=self.parent)
+ print(f"Restored and applied TDP settings: Fast={fast_limit}W, Slow={slow_limit}W")
+ return True
+ else:
+ print("No saved TDP settings found")
+ return False
+ except Exception as e:
+ print(f"Error restoring TDP settings: {e}")
+ return False
+
def apply_current_settings(self, include_advanced=False):
"""Apply current TDP settings"""
try:
@@ -263,6 +342,9 @@ def apply_current_settings(self, include_advanced=False):
# Apply the settings
apply_tdp_settings(profile, parent=self.parent)
+
+ # Save basic settings (fast/slow limits) for auto-restore
+ self.save_tdp_settings(profile)
except (ValueError, TypeError) as e:
print(f"Error applying settings: {e}")
@@ -307,32 +389,19 @@ def load_profiles(self):
return profiles # Return the loaded profiles
def update_profile_dropdown(self):
- if self.cached_profiles:
+ if self.cached_profiles is not None:
self.profile_dropdown.clear()
+ self.profile_dropdown.addItem("— No aplicar —")
for profile in self.cached_profiles:
self.profile_dropdown.addItem(profile["name"])
+ self.profile_dropdown.setCurrentIndex(0)
- # Select first profile by default if none is selected
- if (
- self.profile_dropdown.currentIndex() == -1
- and self.profile_dropdown.count() > 0
- ):
- self.profile_dropdown.setCurrentIndex(0)
-
- # Fixed method to properly handle the signal
def on_profile_select(self, index):
- """Handle profile selection from dropdown"""
- if (
- index < 0
- or not self.cached_profiles
- or index >= len(self.cached_profiles)
- ):
+ """Handle profile selection from dropdown. Index 0 = no aplicar."""
+ if index <= 0 or not self.cached_profiles or index > len(self.cached_profiles):
return
-
- selected_profile = self.cached_profiles[index]
+ selected_profile = self.cached_profiles[index - 1]
self.current_profile = selected_profile
-
- # Update the entries with profile values
self.fast_limit_entry.setText(str(self.current_profile["fast-limit"]))
self.slow_limit_entry.setText(str(self.current_profile["slow-limit"]))
self.slow_time_entry.setText(str(self.current_profile["slow-time"]))
@@ -344,9 +413,8 @@ def on_profile_select(self, index):
self.current_profile["max-performance"]
)
self.power_saving_var.setChecked(self.current_profile["power-saving"])
-
- # Apply the profile
apply_tdp_settings(self.current_profile, parent=self.parent)
+ self.save_tdp_settings(self.current_profile)
def save_profile(self):
profile_name, ok = QInputDialog.getText(
diff --git a/src/app/system_utils.py b/src/app/system_utils.py
index 67fabbd..270f261 100644
--- a/src/app/system_utils.py
+++ b/src/app/system_utils.py
@@ -3,19 +3,26 @@
import os
from PyQt6.QtCore import QProcess
+# Only warn once per session to avoid spamming when nbfc/sensors are missing
+_nbfc_warned = False
+_sensors_warned = False
+
def get_system_readings():
+ global _nbfc_warned, _sensors_warned
# Get temperature and fan speed from NBFC
try:
output = subprocess.check_output(["nbfc", "status", "-a"], text=True)
- except subprocess.CalledProcessError as e:
- print(f"Failed to execute 'nbfc status -a': {e}")
+ except subprocess.CalledProcessError:
temp, fan_speed, profile = "n/a", "n/a", "n/a"
+ if not _nbfc_warned:
+ _nbfc_warned = True
+ print("NBFC service not running. Start it or select a config in the app (e.g. Lenovo ThinkPad T14 Gen2).")
except FileNotFoundError:
- print(
- "nbfc command not found. Make sure NoteBook FanControl is installed."
- )
temp, fan_speed, profile = "n/a", "n/a", "n/a"
+ if not _nbfc_warned:
+ _nbfc_warned = True
+ print("nbfc not found. Install it for fan control (the installer can do this).")
else:
temperature_match = re.search(r"Temperature\s+:\s+(\d+\.?\d*)", output)
fan_speed_match = re.search(r"Current Fan Speed\s+:\s+(\d+\.?\d*)", output)
@@ -28,16 +35,17 @@ def get_system_readings():
profile = (
current_profile_match.group(1) if current_profile_match else "n/a"
)
-
- # Get power consumption data using sensors
+
+ # Get power consumption data using sensors (lm-sensors)
try:
sensors_output = subprocess.check_output(["sensors"], text=True)
power_match = re.search(r"power1:\s+(\d+\.\d+)\s*W", sensors_output)
power = power_match.group(1) if power_match else "n/a"
- except (subprocess.CalledProcessError, FileNotFoundError) as e:
- print(f"Failed to get power data: {e}")
+ except (subprocess.CalledProcessError, FileNotFoundError):
power = "n/a"
-
+ if not _sensors_warned:
+ _sensors_warned = True
+ print("Power data unavailable (install lm-sensors and run 'sensors-detect' for optional power readout).")
return temp, fan_speed, profile, power
diff --git a/src/main.py b/src/main.py
index f61df2f..8d3d6f3 100644
--- a/src/main.py
+++ b/src/main.py
@@ -54,15 +54,19 @@ def main():
except Exception as e:
print(f"Error detecting KDE theme: {e}")
- # Another approach for dark mode detection
+ # Another approach for dark mode detection (PyQt6)
if not is_dark_mode:
try:
- from PyQt5.QtGui import QPalette
+ from PyQt6.QtGui import QPalette
app_palette = app.palette()
# If text is lighter than background, we're likely in dark mode
- bg_color = app_palette.color(QPalette.Window).lightness()
- text_color = app_palette.color(QPalette.WindowText).lightness()
+ bg_color = app_palette.color(
+ QPalette.ColorGroup.Active, QPalette.ColorRole.Window
+ ).lightness()
+ text_color = app_palette.color(
+ QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText
+ ).lightness()
is_dark_mode = text_color > bg_color
except Exception as e:
print(f"Error using palette for theme detection: {e}")
diff --git a/src/version.py b/src/version.py
index 7666d13..dca6b7c 100644
--- a/src/version.py
+++ b/src/version.py
@@ -1,7 +1,7 @@
# src/version.py
VERSION_MAJOR = 1
-VERSION_MINOR = 0
-VERSION_PATCH = 15
+VERSION_MINOR = 2
+VERSION_PATCH = 0
VERSION_TUPLE = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
__version__ = ".".join(map(str, VERSION_TUPLE))
diff --git a/tdp_profiles/LenovoV15-22W.json b/tdp_profiles/LenovoV15-22W.json
new file mode 100644
index 0000000..b2a1990
--- /dev/null
+++ b/tdp_profiles/LenovoV15-22W.json
@@ -0,0 +1,10 @@
+{
+ "name": "LenovoV15-22W",
+ "fast-limit": 22,
+ "slow-limit": 22,
+ "slow-time": 60,
+ "tctl-temp": 70,
+ "apu-skin-temp": 50,
+ "max-performance": true,
+ "power-saving": false
+}
\ No newline at end of file
diff --git a/tdp_profiles/Por-defecto-15W.json b/tdp_profiles/Por-defecto-15W.json
new file mode 100644
index 0000000..90c1bf6
--- /dev/null
+++ b/tdp_profiles/Por-defecto-15W.json
@@ -0,0 +1,10 @@
+{
+ "name": "Por defecto (15W)",
+ "fast-limit": 15,
+ "slow-limit": 15,
+ "slow-time": 60,
+ "tctl-temp": 70,
+ "apu-skin-temp": 50,
+ "max-performance": false,
+ "power-saving": false
+}
diff --git a/version.txt b/version.txt
index 524cb55..26aaba0 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.1.1
+1.2.0