-
-
Notifications
You must be signed in to change notification settings - Fork 6
Developer Documentation
Internals of UXTU4Linux: how the pieces fit together, what each module does, and what to touch when adding hardware support.
Third-party deps: pyzmq (IPC), textual (TUI), textual-plotext (Home graphs). Everything else is stdlib.
Modules are grouped by responsibility under Assets/. Imports are absolute from the Assets package root — from Assets.core import config as cfg.
UXTU4Linux/
├── UXTU4Linux.py entry point: single-instance lock, integrity check, launch TUI
├── requirements.txt pyzmq, textual, textual-plotext
└── Assets/
├── config.ini generated on first run
├── custom.json saved custom presets, generated on first run
├── adaptive.json saved Adaptive Mode presets, generated on first run
├── amd/
│ └── smu.py low-level ryzen_smu sysfs / SMN register interface
├── core/
│ ├── config.py paths, ConfigParser singleton, interval clamp, REQUIRED schema
│ ├── hardware.py CPU detection, dmidecode helpers, Secure Boot, battery sysfs
│ └── ipc.py DaemonClient (ZMQ REQ wrapper) + get_client() singleton
├── daemon/
│ ├── daemon.py PowerDaemon assembly, ZMQ REP loop, main() / startup
│ ├── util.py daemon free functions: SMU apply, preset resolution, lock, _on_ac
│ ├── loops.py LoopsMixin: reapply / power-state / suspend loops
│ ├── commands.py CommandsMixin: the _cmd_* IPC handlers + handle()
│ ├── adaptive.py AdaptiveMixin: Adaptive Mode loop and commands
│ └── service.py systemd unit install/uninstall/restart, venv bootstrap
├── engine/
│ ├── presets.py built-in preset tables, family dispatch, RYZEN_FAMILY
│ ├── adaptive.py Adaptive Mode control maths (power ramp, CO, iGPU clock)
│ └── runner.py SMU command tables per socket, apply_args()
├── flows/
│ ├── setup.py first-run integrity check, config defaults, reset_all
│ └── updater.py version check, stable/beta download, self-update
├── system/
│ ├── sensors.py hwmon / pm_table / sysfs reads, capabilities()
│ ├── pmtable.py ryzen_smu pm_table parsing
│ ├── nvcheck.py NVIDIA dGPU presence and ROP check
│ └── platformctl.py power profile, ASUS WMI, CCD affinity (runs inside the daemon)
├── tuning/
│ ├── power.py built-in preset list, apply dispatch to the daemon
│ ├── custom.py custom-preset field definitions, JSON I/O, build_args()
│ ├── adaptivemanager.py Adaptive Mode preset storage (adaptive.json)
│ └── automations.py AC / battery / resume slot state helpers
└── tui/
├── app.py U4LApp: TabbedContent, key bindings, status refresh loop
├── tabs/ one module per tab (homeView, premadeView, customView, adaptiveView, …)
├── modals.py About, Updater, Sudo, confirm, log and hardware-info modals
├── wizard.py first-run SetupWizard (3-step ModalScreen)
├── helpers.py banner, status line, do_apply(), adaptive_running()
└── app.tcss Textual CSS stylesheet
Runs as your normal user:
- Acquires a single-instance lock with
flockon/tmp/uxtu4linux_tui.lock— a second TUI exits immediately. - Runs
check_binaries()— verifies dmidecode is in PATH; non-Noneresult goes to the TUI asFatalErrorModal. - Runs
check_integrity()andinit_config()fromflows/setup.pyto verifyconfig.iniandcustom.jsonexist with required keys. - Checks
needs_setup()for the first-run wizard, andservice_path_stale()for a moved install. - Launches the Textual app via
Assets.tui.app.run(...).
After mount, app.py's _deferred_startup() calls check_ryzen_smu() from core/hardware.py. Returns None immediately when Secure Boot is off (PCI backend, no module check needed). With Secure Boot on, it inspects ryzen_smu and returns an error string if missing, too old, or unsigned. Non-None triggers FatalErrorModal.
The app's run() returns a string result; "reset" triggers reset_all() and a relaunch, "setup-done" continues normally.
Two separate processes:
TUI (Assets/tui/) — runs as your normal user, handles all interaction, never touches CPU registers.
Daemon (Assets/daemon/daemon.py) — runs as root via systemd, the only process that writes to the CPU. Binds a ZeroMQ REP socket at ipc:///run/uxtu4linux.sock.
Split because SMU writes need root — keeping the TUI unprivileged reduces attack surface, and the daemon validates everything before acting. Socket is chmoded 666 at startup so the TUI can connect. Both sides take single-instance locks: /tmp/uxtu4linux_tui.lock (TUI) and /run/uxtu4linux_daemon.lock (daemon).
DaemonClient wraps a ZeroMQ REQ socket. Send and receive both have a 2-second timeout (TIMEOUT_MS = 2_000). On any error (ZMQ exception, JSON parse failure, timeout, missing socket), _send() closes the socket and returns None; next call creates a fresh connection. Every public method returns a fallback value when the daemon isn't available.
from Assets.core.ipc import get_client
client = get_client()
if client.ping():
result = client.apply(args="--tctl-temp=90", mode="Balance")get_client() returns a module-level singleton guarded by a threading.Lock.
| Command | Payload fields | Returns |
|---|---|---|
ping |
none | {"ok": True, "version": "..."} |
apply |
args, mode
|
{"ok": True, "output": "...", "rejected": bool} |
apply_loop |
args, mode, interval, automation
|
{"ok": True} |
stop_loop |
none | {"ok": True} |
status |
none |
mode, args, running_loop, interval, on_ac, automation, last_output, last_rejected, version, backend, adaptive
|
apply_saved |
none | re-derives all runtime state from config: stops the loop/monitor, then starts the loop, the monitor, a single apply, or goes idle - whichever config implies |
reload_config |
none | {"ok": True} |
reset_state |
none | stops the loop and clears the in-memory preset (used by Reset all) |
dmidecode |
type |
{"ok": True, "output": "..."} |
adaptive_start |
preset, optional values
|
{"ok": True, "caps": [...]} |
adaptive_stop |
none | {"ok": True, "reverted": bool} |
dmidecode is proxied through the daemon because dmidecode -t needs root. Allowed type strings are whitelisted in _DMI_ALLOWED_TYPES in commands.py.
To add an IPC command: add the _cmd_* handler to the right daemon mixin (commands.py for general, adaptive.py for adaptive), add a _dispatch entry in PowerDaemon.__init__, then the matching client method in ipc.py.
PowerDaemon (daemon.py) is assembled from three mixins — CommandsMixin (commands.py), LoopsMixin (loops.py), AdaptiveMixin (adaptive.py) — over the free functions in util.py. Manages the ZMQ server loop and up to four background threads. All shared state is under self._lock.
First three live in LoopsMixin (loops.py); adaptive loop in AdaptiveMixin (adaptive.py).
Reapply loop (_loop_body) — started by apply_loop. Calls _apply_once() every N seconds. With an automation slot set, picks the correct preset for the current AC/battery state each tick and logs on transitions.
Power monitor (_monitor_body) — polls /sys/class/power_supply for AC state changes, applies the matching slot, skips empty slots. Runs independently of the reapply loop.
Suspend monitor (_suspend_monitor_body) — compares CLOCK_BOOTTIME against time.monotonic(). Gap exceeds threshold → system just resumed → reads OnResume from config and applies it. Catches suspend-to-RAM and hibernation with no logind dependency. Logs and disables itself if CLOCK_BOOTTIME isn't available.
Adaptive loop (_adaptive_body) — started by adaptive_start. Each tick: samples sensors, computes dynamic args (engine/adaptive.py) plus ASUS/NVIDIA args, applies them. Reapply loop and power monitor skip their ticks while this runs.
_effective_mode_args() resolves which preset actually applies given the base preset, automation slots, and current power state. On transitions it's called with keep_on_empty=True so an empty slot leaves current settings untouched. _load_saved_preset() reconstructs state from config on startup and apply_saved.
apply_saved is the resync entry point — fully re-derives runtime state from config, so the TUI just writes config and calls apply_saved. Automation is active whenever OnAC or OnBattery is set; no separate flag. Every command path leaves coherent state.
The single place CPU settings are written. Returns (output, rejected) — output is the per-command status log, rejected is True if the SMU refused anything. When reason is set and the apply succeeded, the daemon logs one line:
Applied preset 'Eco' (power source changed from AC to battery).
Applied preset 'Balance' (woke from suspend after ~28m).
Steady-state reapply ticks pass no reason and stay at debug level, so the journal isn't flooded. The method updates self._mode, self._args, self._last_output and self._last_rejected under the lock - these are what the status command reports.
self._dispatch maps command names to _cmd_* methods. handle(raw) parses the JSON, looks up the command, calls it, returns the JSON response (exceptions become {"ok": False, "error": ...}). The run() loop receives on the REP socket, calls handle(), sends the response, breaks on ZMQ error. SIGTERM/SIGINT stop threads, unlink socket, exit.
main() logs an actionable message for each problem:
-
Secure Boot off → logs
Secure Boot disabled — using PCI direct access backend., callsinit_pci_backend(). If PCI unavailable →PCI backend unavailable — presets will not be applied. - Secure Boot on → checks ryzen_smu; logs version, "too old", "not installed. Required when Secure Boot is enabled.", "not signed for Secure Boot.", "installed but not loaded." as appropriate.
- dmidecode missing → exits with error
- Intel CPU → warning, continues (presets won't apply)
- Another daemon instance holds
/run/uxtu4linux_daemon.lock→ exits
These exact strings are quoted in the Linux Troubleshooting table — update that page if you change them.
Two backends, selected at daemon startup based on Secure Boot state:
PCI backend (Secure Boot off)
init_pci_backend() is called when secure_boot_enabled() returns False. Opens /sys/bus/pci/devices/0000:00:00.0/config (northbridge PCI config space) and performs SMN register reads/writes via the NB address/data port pair at 0xB8 / 0xBC. No kernel module needed. active_backend() returns "pci".
ryzen_smu backend (Secure Boot on)
When Secure Boot is on, the daemon uses the ryzen_smu sysfs interface at /sys/kernel/ryzen_smu_drv/:
| Path | Purpose |
|---|---|
/sys/kernel/ryzen_smu_drv/ |
Module presence check |
.../drv_version |
Module version string |
.../smn |
System Management Network register interface |
UXTU4Linux drives the SMU through SMN register writes on the smn node. active_backend() returns "ryzen_smu".
Both backends share the same protocol: two channels, MP1 (management processor) and RSMU (register-based SMU). Both follow write args → write opcode → poll response. Mailbox register addresses differ per family and live in _MP1 / _RSMU.
PM table reading on the PCI backend: read_pm_table_pci(family) sends three RSMU commands (get version → get DRAM address → trigger DMA), then reads the table from the physical address via /dev/mem.
status = smu.send_mp1(family, op_code, value)
status = smu.send_rsmu(family, op_code, value)| Constant | Value | Meaning |
|---|---|---|
SMU_OK |
0x01 | Command accepted |
SMU_FAILED |
0xFF | General failure |
SMU_UNKNOWN_CMD |
0xFE | Opcode not recognized |
SMU_REJECTED_PREREQ |
0xFD | Prerequisite not met |
SMU_REJECTED_BUSY |
0xFC | SMU is busy |
All SMN access is serialized under a module-level threading.Lock. Poll spins tightly first, then sleeps in small steps with a deadline.
MIN_VERSION = (0, 1, 7)
smu.is_available() # True if /sys/kernel/ryzen_smu_drv/ exists
smu.version_ok() # True if version >= MIN_VERSIONMaps CPU families to SMU command tables. _FAMILY_SOCKET maps a family to a socket group; each socket group has a _CMD_* table mapping arg names to (is_mp1, opcode). is_mp1 = True → MP1, False → RSMU. Tables are kept in sync with the original UXTU's RyzenSmu.cs.
Family → socket type:
| Family | Socket |
|---|---|
| SummitRidge, PinnacleRidge | AM4_V1 |
| RavenRidge, Picasso, Dali, Pollock, FireFlight | FT5_FP5_AM4 |
| Matisse, Vermeer | AM4_V2 |
| Renoir, Lucienne, Cezanne_Barcelo | FP6_AM4 |
| VanGogh | FF3 |
| Mendocino, Rembrandt, PhoenixPoint, PhoenixPoint2, HawkPoint, HawkPoint2, SonomaValley, StrixPoint, StrixHalo, KrackanPoint, KrackanPoint2 | FT6_FP7_FP8 |
| Raphael, DragonRange, GraniteRidge, FireRange | AM5_V1 |
runner.lookup(family, arg_name) -> list[tuple[bool, int]] # [] if not supported
runner.has_smu_support(family) -> bool
runner.apply_args(args_str, family) -> tuple[str, bool] # (output_log, any_rejected)apply_args is what _apply_once calls:
-
shlex.splitthe args - Strip
--, split on= - Route
sys-*tokens to_apply_system()→platformctl.py,nvidia-clocksto_apply_nvidia() - Otherwise
runner.lookup(family, name)for the opcode - Scale value if needed (skin temp × 256)
- Send via
smu.send_mp1()/smu.send_rsmu() - Collect per-command status; SMU-rejected commands marked
[!]
Unsupported args: not supported on <family>, skipped. rejected flag set only when hardware returns non-OK for a sent command.
NVIDIA: --nvidia-clocks=max,core,mem → _apply_nvidia(). Uses nvidia-smi -lgc 0,<max> to cap clock (-rgc to reset when max is 4000), NVML (libnvidia-ml.so.1) for offsets. Tries nvmlDeviceSetClockOffsets (new) then nvmlDeviceSetGpcClkVfOffset / nvmlDeviceSetMemClkVfOffset (legacy).
Pseudo-args prefixed sys- are not SMU commands. runner.apply_args() routes them through platformctl.py, which runs inside the daemon (root):
| Arg | Backend |
|---|---|
sys-power-profile=0|1|2 |
/sys/firmware/acpi/platform_profile, falling back to powerprofilesctl, then tuned-adm
|
sys-asus-mode=0|1|2 |
ASUS throttle_thermal_policy (asus-nb-wmi sysfs or asus-armoury firmware-attributes) |
sys-asus-eco=0|1 |
ASUS dgpu_disable
|
sys-asus-mux=0|1 |
ASUS gpu_mux_mode
|
sys-ccd-affinity=0|1|2 |
systemctl set-property --runtime user.slice AllowedCPUs=... |
-
resolve_profile()maps canonical profile names to whatever the firmware offers, using the same synonym table as G-Helper (platform_profile_choices). -
set_power_profile()tries sysfs first, thenpowerprofilesctl(covers power-profiles-daemon and Fedora's tuned-ppd), thentuned-adm. -
tlp_profile_conflict()checks/etc/tlp.confforPLATFORM_PROFILE_ON_AC/BAT; if set, logs a warning that TLP will fight over the profile. - ASUS paths try three candidates: asus-nb-wmi platform device, generic platform bus, asus-armoury firmware-attributes (kernel 6.8+).
-
set_asus_eco()carries G-Helper's safety guards (refuses while the dGPU driver is active or MUX is in Ultimate mode) and triggers a PCI bus rescan after re-enabling the dGPU. -
_l3_domains()readsshared_cpu_listto find CCDs;ccd_affinity_available()is true only with two or more L3 domains. - Each setter caches its last written value and skips the write if nothing changed.
Each built-in preset is a space-separated string of ryzenadj-style args (e.g. "--tctl-temp=90 --stapm-limit=25000 ..."), ported from the original UXTU's PremadePresets.cs. The dead --vrmgfx-current arg is dropped, and --Win-Power=N maps to --sys-power-profile=N.
@dataclass
class Preset:
Eco: str
Balance: str
Performance: str
Extreme: strget_preset(cpu_type, family, cpu_model, raw_cpu, variant) dispatches in this order:
-
Variant match
_variant_preset(variant)- specific devices like Framework Laptop models -
APU family
_apu_preset(family, cpu_model)- dispatches on model suffix (U/H/HS/HX/G/GE) -
Desktop CPU
_desktop_preset(family, cpu_model) -
Fallback
_desktop_standard()
RYZEN_FAMILY lists every known family in release order; _before(family, ref) returns True if family is earlier than ref, used for conditions like "any family before Matisse".
- Add the family to
RYZEN_FAMILYat the right position - Add the family→socket entry in
_FAMILY_SOCKETinrunner.py(and a command table if it's a new socket) - Add MP1/RSMU register addresses to
_MP1/_RSMUinsmu.pyif different from defaults - Add a branch in
_apu_preset()/_desktop_preset()returning aPreset - Add the label in
get_preset_label() - Add the codename resolution in
_resolve_codename()inhardware.py
Assets/custom.json is a JSON array of preset objects:
[
{
"name": "My Preset",
"tctl_temp": {"enabled": true, "value": 90},
"stapm_limit": {"enabled": true, "value": 25}
}
]Values are stored in display units (W, A, °C, MHz); build_args() applies the conversion (times 1000 for W and A) when generating the arg string. Internally, custom preset names get a _custom_preset suffix to distinguish them from built-ins; the UI strips it for display via display_name().
FIELD_DEFS (APU) and FIELD_DEFS_DT (desktop) are lists of dicts, one per parameter:
{
"key": "stapm_limit", "label": "STAPM Power Limit", "arg": "--stapm-limit",
"unit": "W", "default": 28, "min": 5, "max": 300, "step": 1,
"enabled": False, "section": 2, "hint": "...",
}Special properties: scale (multiply before sending), signed_co (negative CO uses 0x100000 - abs(v)), ccd/core (per-core CO indices), choices (string list; stored value is an index), nvidia_only, check_arg (alternate arg name for support lookup), system_check (System-section field, shown only when the capability is detected).
Section titles come from APU_SECTION_TITLES / DT_SECTION_TITLES. _supported_field_keys() filters fields by runner.lookup() and _system_supported(); _active_sections() keeps only sections that still have a visible field — that's what drives the editor's collapsible sections.
Iterates enabled fields. Special cases: tctl_temp on APUs also emits --chtc-temp; oc_clk/oc_volt are emitted twice each plus --enable-oc --enable-oc; --set-coper packs CCD index, core index and CO value into one 32-bit int; NVIDIA fields collapse into a single --nvidia-clocks=max,core,mem; System fields emit their --sys-* arg with the choice index.
save_preset() checks if the preset is currently active; if so, tells the daemon to apply_saved so the new values take effect immediately. delete_preset() clears config references, disables automations if both AC/battery slots are now empty, and notifies the daemon.
config.py owns the single ConfigParser instance over Assets/config.ini. Both processes read it; only the TUI writes it.
from Assets.core import config as cfg
val = cfg.get("Settings", "Time", "3") # fallback "" if omitted
cfg.set_config("Settings", "Time", "5")
cfg.save() # atomic
cfg.load() # re-read from disk
if cfg.is_debug(): ...Saves are atomic: atomic_write() writes a temp file, fsyncs, then os.replace()s it over the target.
cfg.CONFIG_PATH # <Assets>/config.ini
cfg.CUSTOM_PRESETS_PATH # pathlib.Path to custom.json
cfg.VENV_DIR # /opt/uxtu4linux/venv
cfg.VENV_PYTHON # /opt/uxtu4linux/venv/bin/python3
cfg.ZMQ_SOCKET_PATH # /run/uxtu4linux.sock
cfg.ZMQ_SOCKET_ADDR # ipc:///run/uxtu4linux.sock
cfg.LOCAL_VERSION # current version string (e.g. "0.9.0")
cfg.LOCAL_BUILD # build tagparse_interval() clamps the reapply interval to [MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS] = [1, 86400]. REQUIRED documents the keys each section must have.
check_integrity() runs at TUI startup:
- File missing/empty → run the setup wizard (
needs_setup()returns True) -
Infosection missing or incomplete →reset_all()(deletesconfig.iniandcustom.json), then wizard - Other missing keys → filled from defaults and saved, no wizard
Adding new config keys in a future version won't wipe settings — missing keys are filled silently.
detect() runs during setup and from Settings → Re-detect hardware:
- Reads CPU name (
Version) andSignaturefromdmidecode -t processorvia IPC, stores inInfo.CPU/Info.Signature -
_compute_codename()parses Family/Model integers, calls_resolve_codename()for(arch, family), then_cpu_type()forAmd_Apu/Amd_Desktop_Cpu/Intel/Unknown. AMD family with norunner.has_smu_support()→ downgraded toUnknown. - Framework Laptop variant via
_detect_framework_variant()→Info.Variant cfg.save()
| cpu_family | Architecture |
|---|---|
| 23 | Zen 1 - Zen 2 |
| 25 | Zen 3 - Zen 4 |
| 26 | Zen 5 - Zen 6 |
Some models need the CPU name to disambiguate — family 25 / model 97 is DragonRange if the name contains "HX", otherwise Raphael.
_DESKTOP_FAMILIES = {"SummitRidge", "PinnacleRidge", "Matisse", "Vermeer", "Raphael", "GraniteRidge"}hardware.py also holds ryzen_smu install/sign/load checks and Secure Boot detection.
Built on Textual. Runs as the normal user, talks to the daemon through core/ipc.py.
U4LApp(App) loads app.tcss and composes a banner, a status line, and a TabbedContent with eight TabPanes: home, power, custom, adaptive, automations, hardware, status, settings. Key bindings: h Home, 1–7 for the other tabs, ? About, q quit.
-
set_interval(1.0, …)polls daemon status and updates the status line every second; offline daemon shows a one-time warning toast. -
_deferred_startup()runs in a background thread after mount: callscheck_ryzen_smu(), checks for a stale service path, callsapply_savedif the daemon has no active preset, and compares daemon version againstcfg.LOCAL_VERSION. If they differ, fires a warning toast. -
_check_size()hides the banner/tabs and shows a "terminal too small" notice below 50×25, swaps the full banner for a compact wordmark on narrow terminals. - On mount: applies saved theme and pushes
FatalErrorModal,SetupWizard,StalePathModal, or a startup update prompt depending on state. - A background toast-reaper thread wakes the event loop so notifications dismiss on time.
One module per tab. Each is a VerticalScroll subclass; app.py imports each directly.
-
HomeTab (
homeView.py) — live CPU temp/power/clock/usage graphs (textual-plotext), iGPU graphs on APUs, and navigation buttons. -
PowerTab (
premadeView.py) — four preset buttons (Eco/Balance/Performance/Extreme); clicking applies throughhelpers.do_apply. Blocked while Adaptive Mode runs. -
CustomEditor (
customView.py) — Saved Presets select, name input, Save/Apply/Duplicate/Delete, collapsible sections of field cards (Switch + Input or Select per parameter). -
AdaptiveTab (
adaptiveView.py) — Save/Duplicate/Delete/Start over collapsible settings; ASUS and NVIDIA sections visible only when that hardware is detected. -
AutomationsTab (
automationsView.py) — three Select slots (Battery Charge / Battery Discharge / System Resume). No enable switch; active whenever a slot is set. -
HardwareTab (
infoView.py) — CPU, memory, cache and battery viaclient.dmidecode(...). -
StatusTab (
statusView.py) — daemon state, automation slots, adaptive state and SMU output in one panel. Refreshes every second while open, plus a power-source watcher that refreshes on AC/battery flip. -
SettingsTab (
settingsView.py) — general toggles (ApplyOnStart, AutoStartAdaptive, SoftwareUpdate, Debug), reapply controls, default-tab select, daemon service card (Install/repair, Restart, View logs, Uninstall), hardware/reset card.
modals.py: AboutModal, UpdaterModal, UpdateProgressModal, SudoModal (in-app password prompt for service.prime_sudo), ConfirmModal, DaemonLogModal, HardwareInfoModal, FatalErrorModal, StalePathModal. wizard.py is the three-step SetupWizard (Welcome → Background daemon → Detect hardware).
do_apply() sends an apply through the daemon, ensure_sudo() runs the SudoModal flow, status_line() / fetch_status() build the header, BANNER / WORDMARK are the ASCII art.
install_service() # ensure venv, write unit, daemon-reload, enable, start
uninstall_service() # stop, disable, remove the unit file, daemon-reload
restart_service()
regenerate_service() # rewrite the unit and restart (used when the path is stale)
service_path_stale() # True if the unit's ExecStart no longer matches the current paths
read_logs(lines=200) # journalctl -u uxtu4linux.service_ensure_venv() creates /opt/uxtu4linux/venv via python3 -m venv --without-pip, bootstraps pip via ensurepip, installs pyzmq, textual and textual-plotext if they're not importable. _render_unit() bakes in the absolute path to the venv Python and to daemon.py, so the unit stays correct if the app is moved. Unit file is written via temp file + sudo mv. prime_sudo() / sudo_available() handle credential caching for the in-app SudoModal.
verify_service_path() runs at TUI startup: if the install moved and the unit's ExecStart no longer matches, it regenerates and restarts. Non-systemd systems get the manual daemon-start command instead.
Versions are MAJOR.MINOR.PATCH; pre-release/build suffixes are stripped before comparison. _ver_tuple(v) parses each segment. get_latest_version() follows the GitHub releases/latest redirect and validates the tag. check_updates() retries a few times before giving up.
Beta builds: is_beta_build() / get_beta_commit() work against the rolling U4L-Beta tag; "Switch to beta" downloads that release asset.
_do_update procedure:
- Back up
config.iniandcustom.jsoninto/opt/uxtu4linux/ - Download and extract the release zip to
<install dir>/UXTU4Linux_new/ -
sudo mvcurrent app dir aside as/opt/uxtu4linux/src.bak -
sudo mvnew release into place; on failure.bakis moved back - Remove
.bakandUXTU4Linux_new/ - Move config/preset backups into the new
Assets/ - Remove the zip; reinstall
requirements.txtinto the venv - Restart daemon if the service is running
-
os.execvto relaunch the TUI from new code
On success the only backups left are the two config files in the install directory.
Manual (workflow_dispatch). Zips UXTU4Linux/, reads version and notes from Changelog.md, generates release notes (install one-liner + changelog), publishes the GitHub release that install.sh downloads.
Also manual. Packages current state, deletes the existing U4L-Beta release and tag, force-creates the tag at HEAD, publishes a pre-release titled "U4L Beta Build". "Switch to beta" downloads from this release.
Versioning lives in two places that must stay in sync:
LOCAL_VERSION/LOCAL_BUILDincore/config.pyand the top entry ofChangelog.md.
- Python 3.10+, type hints on all function signatures
- Error handling only at system boundaries (user input, IPC, file I/O)
- No backwards-compatibility shims for removed features
- Daemon log strings and install instructions are quoted in the wiki; keep them in sync
- Add the family to
RYZEN_FAMILYinengine/presets.pyat the right position - Add the family→socket entry in
_FAMILY_SOCKETinengine/runner.py - Add the command table under the socket type in
runner.py(or create a new one) - Add MP1/RSMU register addresses to
_MP1/_RSMUinamd/smu.pyif different from defaults - Add the
_resolve_codenamebranch incore/hardware.pywith the rightcpu_family/cpu_model - Add the CPU type classification if it's a new desktop family
- Add the
_apu_preset()/_desktop_preset()branch inpresets.py - Add the label in
get_preset_label() - Test on a machine that has the CPU
sudo dmidecode -t processor | grep SignatureOutput: Signature: Type 0, Family 26, Model 32, Stepping 1. Family and Model go into _resolve_codename().
No build step. Run the TUI directly:
python3 UXTU4Linux/UXTU4Linux.pyDaemon needs root; for development run it by hand:
sudo python3 UXTU4Linux/Assets/daemon/daemon.pySet Debug = 1 under [Settings] (or toggle it in Settings) for DEBUG-level daemon logs; picked up on reload_config. The installer puts a release zip at /opt/uxtu4linux/src, so test working-tree changes by running source directly.
Getting started
Using the app
Internals