Add Framework laptop controls and monitoring with hardware-aware discovery

This commit is contained in:
ajp_anton
2026-09-11 10:29:27 +00:00
parent 787231cb03
commit 8719402d97
50 changed files with 3611 additions and 9 deletions
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: MIT
#include "cpupage.h"
#include <QFormLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
QVariantMap CpuPage::Editor::value() const
{
QVariantMap result{{"frequencyOverride", frequency->isChecked() && bounds.isEmpty()}, {"minimum", minimum->value()}, {"maximum", maximum->value()},
{"governor", governor->currentText()}, {"preference", preference->currentText()}};
if (!bounds.isEmpty()) {
QVariantMap groups;
for (auto it = bounds.cbegin(); it != bounds.cend(); ++it) groups[it.key()] = QVariantMap{
{"frequencyOverride", it->enabled->isChecked()}, {"minimum", it->minimum->value()}, {"maximum", it->maximum->value()}};
result["bounds"] = groups;
}
return result;
}
void CpuPage::Editor::setValue(const QVariantMap &value)
{
minimum->setValue(value["minimum"].toInt()); maximum->setValue(value["maximum"].toInt());
frequency->setChecked(value.value("frequencyOverride").toBool());
minimum->setEnabled(frequency->isChecked()); maximum->setEnabled(frequency->isChecked());
governor->setCurrentText(value.value("governor", "auto").toString()); preference->setCurrentText(value.value("preference", "auto").toString());
for (auto it = bounds.cbegin(); it != bounds.cend(); ++it) {
const auto p = value.contains("bounds") ? value["bounds"].toMap().value(it.key()).toMap() : value;
it->minimum->setValue(p.value("minimum", it->minimum->minimum()).toInt());
it->maximum->setValue(p.value("maximum", it->maximum->maximum()).toInt());
it->enabled->setChecked(p.value("frequencyOverride").toBool());
it->minimum->setEnabled(it->enabled->isChecked()); it->maximum->setEnabled(it->enabled->isChecked());
}
}
CpuPage::Editor CpuPage::makeEditor(const QString &name)
{
Editor editor{new QGroupBox(name), new QCheckBox("Override frequency bounds"),
new ValueControl, new ValueControl, new QComboBox, new QComboBox, new QLabel};
editor.frequency->setObjectName("overrideFrequency");
editor.governor->setObjectName("cpuGovernor"); editor.preference->setObjectName("cpuPreference");
editor.warning->setWordWrap(true); editor.warning->setObjectName("governorWarning");
auto *layout = new QFormLayout(editor.group);
const auto limits = cpuLimits();
layout->addRow(editor.frequency);
editor.frequency->setToolTip("When unchecked, this tool does not write frequency limits. Existing limits are left unchanged.");
for (auto *box : {editor.minimum, editor.maximum}) {
box->setRange(limits.value("low", 400).toInt(), limits.value("high", 5000).toInt());
box->setSuffix(" MHz"); box->setSingleStep(100);
connect(box, &ValueControl::valueChanged, this, [this] { changed(); });
}
editor.governor->addItem("auto"); editor.preference->addItem("auto");
editor.governor->addItems(cpuOptions("scaling_available_governors"));
editor.preference->addItems(cpuOptions("energy_performance_available_preferences"));
for (auto *combo : {editor.governor, editor.preference}) {
combo->setToolTip("Auto leaves this setting to Linux or other controllers; it does not restore a preset. Save and apply commits changes.");
connect(combo, &QComboBox::currentTextChanged, this, [this] { changed(); });
}
layout->addRow("Minimum frequency:", editor.minimum); layout->addRow("Maximum frequency:", editor.maximum);
const auto groups = cpuPolicyGroups();
if (!groups.isEmpty()) {
layout->setRowVisible(editor.frequency, false); layout->setRowVisible(editor.minimum, false); layout->setRowVisible(editor.maximum, false);
for (const QString &key : {QString("p"), QString("e")}) {
Editor::Bounds b{new QCheckBox(key == "p" ? "Override P-core frequency bounds" : "Override E-core frequency bounds"), new ValueControl, new ValueControl};
const auto limits = cpuLimits("/sys", key);
for (auto *box : {b.minimum, b.maximum}) {
box->setRange(limits["low"].toInt(), limits["high"].toInt()); box->setSuffix(" MHz"); box->setSingleStep(100);
connect(box, &ValueControl::valueChanged, this, [this] { changed(); });
}
b.enabled->setObjectName("overrideFrequency_" + key);
layout->addRow(b.enabled); layout->addRow("Minimum frequency:", b.minimum); layout->addRow("Maximum frequency:", b.maximum);
connect(b.enabled, &QCheckBox::toggled, this, [this, b](bool enabled) { b.minimum->setEnabled(enabled); b.maximum->setEnabled(enabled); changed(); });
editor.bounds.insert(key, b);
}
}
layout->addRow("Energy preference:", editor.preference);
auto *row = new QWidget; auto *line = new QHBoxLayout(row); line->setContentsMargins(0, 0, 0, 0);
editor.governor->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
line->addWidget(editor.governor); line->addWidget(editor.warning, 1); line->addStretch();
layout->addRow("CPU governor:", row);
connect(editor.frequency, &QCheckBox::toggled, this, [this, editor](bool enabled) {
editor.minimum->setEnabled(enabled); editor.maximum->setEnabled(enabled); changed();
});
return editor;
}
CpuPage::CpuPage(QWidget *parent) : QWidget(parent)
{
auto *layout = new QVBoxLayout(this);
m_status = new QLabel; m_status->setWordWrap(true); layout->addWidget(m_status);
m_separate = new QCheckBox("Use separate profiles for battery power and AC");
m_separate->setObjectName("separateCpuProfiles"); layout->addWidget(m_separate);
m_separate->setToolTip("Splitting copies settings to both sides. Joining keeps the battery settings, committed only with Save and apply.");
auto *columns = new QHBoxLayout;
m_battery = makeEditor("CPU settings"); m_ac = makeEditor("AC power");
m_battery.group->setObjectName("batteryCpuProfile"); m_ac.group->setObjectName("acCpuProfile");
columns->addWidget(m_battery.group, 1); columns->addWidget(m_ac.group, 1); layout->addLayout(columns);
layout->addStretch();
connect(m_separate, &QCheckBox::toggled, this, &CpuPage::split);
load({}, true);
}
void CpuPage::changed()
{
if (!m_loading) {
// Joining hides the AC draft; it must survive refresh until saved or undone.
const auto savedAc = m_saved.value(m_saved.value("separate").toBool() ? "ac" : "battery").toMap();
m_dirty = draft() != m_saved || (m_hasAcDraft && !m_separate->isChecked() && m_ac.value() != savedAc);
refreshStatus(); Q_EMIT draftChanged();
}
}
void CpuPage::split(bool separate)
{
if (separate && !m_hasAcDraft) { m_ac.setValue(m_battery.value()); m_hasAcDraft = true; }
m_ac.group->setVisible(separate); m_battery.group->setTitle(separate ? "Battery power" : "CPU settings");
changed();
}
QVariantMap CpuPage::draft() const
{
QVariantMap config{{"separate", m_separate->isChecked()}, {"battery", m_battery.value()}};
if (m_separate->isChecked()) config["ac"] = m_ac.value();
return config;
}
void CpuPage::load(const QVariantMap &config, bool force)
{
if (m_dirty && !force) return;
m_loading = true;
auto saved = normalizeCpuConfig(config);
if (saved.isEmpty()) {
const auto limits = cpuLimits();
saved = {{"separate", false}, {"battery", QVariantMap{{"frequencyOverride", false},
{"minimum", limits.value("min", 400)}, {"maximum", limits.value("max", 5000)},
{"governor", "auto"}, {"preference", "auto"}}}};
}
m_saved = saved;
m_battery.setValue(saved["battery"].toMap());
m_hasAcDraft = saved["separate"].toBool();
if (m_hasAcDraft) m_ac.setValue(saved["ac"].toMap());
else m_ac.setValue(saved["battery"].toMap());
m_separate->setChecked(m_hasAcDraft); split(m_hasAcDraft);
m_saved = draft(); m_loading = false; m_dirty = false; refreshStatus(); Q_EMIT draftChanged();
}
void CpuPage::refreshStatus()
{
const auto limits = cpuLimits(); const auto ac = onAcPower();
m_status->setText(limits.isEmpty() ? "Live CPU readings unavailable." :
QString("Live: %1\nEnergy preference: %3\nGovernor: %2\nFrequency bounds: %4%5 MHz (highest across policies); hardware maximum: %6 MHz.%7")
.arg(ac ? (*ac ? "AC power" : "battery power") : "power source unknown", limits["governor"].toString(), limits["preference"].toString())
.arg(limits["min"].toInt()).arg(limits["max"].toInt()).arg(limits["high"].toInt())
.arg(m_dirty ? "\nUnsaved changes." : ""));
auto warning = [&](Editor &editor, bool active) {
QString text;
const auto p = editor.value();
const QString governor = p["governor"].toString(), pref = p["preference"].toString();
if (readText("/sys/devices/system/cpu/cpufreq/policy0/scaling_driver") == "intel_pstate") {
if (governor == "performance" && pref == "auto")
text = "⚠ Requires performance EPP; may fall back to powersave.";
else if (governor == "performance" && pref != "performance")
text = "⚠ Requires performance EPP; will fall back to powersave.";
else if (governor == "auto" && pref != "auto" && pref != "performance" && active && limits["governor"] == "performance")
text = "⚠ Current Performance governor blocks this EPP. Select Powersave or leave EPP on Auto.";
}
editor.warning->setText(text); editor.warning->setVisible(!text.isEmpty());
};
warning(m_battery, !m_separate->isChecked() || (ac && !*ac));
warning(m_ac, m_separate->isChecked() && ac && *ac);
}