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
+129
View File
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT
#include "hardware.h"
#include "fan.h"
#include "cpu.h"
#include <KAuth/ActionReply>
#include <KAuth/HelperSupport>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QProcess>
#include <QSaveFile>
class Helper : public QObject {
Q_OBJECT
KAuth::ActionReply reply(const QString &error, const QVariantMap &data = {}) {
if (error.isEmpty()) { auto r = KAuth::ActionReply::SuccessReply(); r.setData(data); return r; }
auto r = KAuth::ActionReply::HelperErrorReply(); r.setErrorDescription(error); return r;
}
QString service(const QString &verb, const QString &unit = "framework-laptop-tools-fan.service") {
QProcess process;
process.start("/usr/bin/systemctl", {verb, unit});
if (!process.waitForFinished(25000)) { process.kill(); process.waitForFinished(); return "Service did not respond."; }
return process.exitCode() == 0 ? QString() : QString::fromUtf8(process.readAllStandardError());
}
public Q_SLOTS:
KAuth::ActionReply inspect(const QVariantMap &) {
const auto error = compatibilityError();
if (!error.isEmpty()) return reply(error);
auto data = firmwareStatus();
QString cpuError;
data["cpuConfig"] = readCpuConfig(cpuError);
if (!cpuError.isEmpty()) data["cpuError"] = cpuError;
const auto path = ecHwmon();
const auto mode = readNumber(path + "/pwm1_enable");
if (mode == std::optional<double>(2)) data["fanMode"] = "auto";
else if (mode == std::optional<double>(1)) {
data["fanMode"] = "manual";
const auto pwm = readNumber(path + "/pwm1");
if (pwm) data["fanDuty"] = qRound(*pwm * 100 / 255);
QString fanError;
const auto config = readFanConfig(fanError);
if (!fanError.isEmpty()) data["fanError"] = fanError;
if (fanError.isEmpty()) {
if (config.value("mode") == "curve") {
data["fanMode"] = "curve";
data["fanCurve"] = config["curve"];
QVariantList temperatures;
for (const double t : fanTemperatures(config)) temperatures.append(t);
data["fanTemperatures"] = temperatures;
}
}
}
data["fanPersistent"] = QFile::exists(savedFanConfigPath);
return reply({}, data);
}
KAuth::ActionReply configure(const QVariantMap &args) {
QString error = compatibilityError();
if (!error.isEmpty()) return reply(error);
const QString operation = args.value("operation").toString();
if (operation == "batch") {
const auto operations = args.value("operations").toList();
if (operations.isEmpty() || operations.size() > 6) return reply("Invalid settings batch.");
const QStringList allowed{"keyboard", "powerAuto", "powerBrightness", "chargeLimit", "chargeWatts", "fan", "cpuProfiles"};
for (const auto &entry : operations)
if (!allowed.contains(entry.toMap().value("operation").toString())) return reply("Unknown operation in settings batch.");
QVariantList completed;
for (const auto &entry : operations) {
const auto result = configure(entry.toMap());
if (result.failed()) return reply({}, {{"completed", completed}, {"applyError", result.errorDescription()}});
completed.append(entry);
}
return reply({}, {{"completed", completed}});
}
bool valid;
const double raw = args.value("value").toDouble(&valid);
const int value = valid && raw >= 0 && raw <= 10000 && raw == int(raw) ? int(raw) : -1;
if (operation == "keyboard" && value >= 0 && value <= 100)
error = writeNumber("/sys/class/leds/chromeos::kbd_backlight/brightness", value);
else if (operation == "cpuProfiles") {
const auto config = normalizeCpuConfig(args.value("config").toMap());
error = validateCpuConfig(config);
if (!error.isEmpty()) return reply(error);
if (!QDir().mkpath("/etc/framework-laptop-tools")) return reply("Cannot create CPU configuration directory.");
QSaveFile file(cpuConfigPath);
if (!file.open(QIODevice::WriteOnly)) return reply(file.errorString());
file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
const auto data = QJsonDocument::fromVariant(config).toJson();
if (file.write(data) != data.size() || !file.commit()) return reply(file.errorString());
const QString unit = "framework-laptop-tools-cpu.service";
if (hasCpuOverrides(config)) {
error = service("enable", unit);
if (error.isEmpty()) error = service("restart", unit);
} else {
error = service("disable", unit);
if (error.isEmpty()) error = service("stop", unit);
}
if (!error.isEmpty()) error = "CPU settings were saved, but applying them failed: " + error;
} else if (operation == "fan") {
error = validateFan(args);
if (!error.isEmpty()) return reply(error);
// Restore the old worker before replacing its configuration.
error = service("stop");
if (!error.isEmpty()) return reply(error);
if (QFile::exists(savedFanConfigPath) && !QFile::remove(savedFanConfigPath)) return reply("Cannot remove saved fan settings.");
const bool persistent = args.value("mode") != "auto";
error = service(persistent ? "enable" : "disable");
if (error.isEmpty()) error = service(persistent ? "enable" : "disable", "framework-laptop-tools-fan-resume.service");
if (!error.isEmpty()) return reply(error);
if (args.value("mode") == "auto") error = restoreFan();
else {
const QString path = persistent ? savedFanConfigPath : fanConfigPath;
if (!QDir().mkpath(QFileInfo(path).absolutePath())) error = "Cannot create fan settings directory.";
if (error.isEmpty()) {
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly)) return reply(file.errorString());
file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
const auto data = QJsonDocument::fromVariant(args).toJson(QJsonDocument::Compact);
if (file.write(data) != data.size() || !file.commit()) return reply(file.errorString());
error = service("start");
}
}
} else if (operation == "powerAuto") error = setFirmwareValue(operation, 0);
else if (value >= 0) error = setFirmwareValue(operation, value);
else error = "Unknown operation or invalid value.";
return reply(error);
}
};
KAUTH_HELPER_MAIN("se.ajpanton.frameworktools", Helper)
#include "helper.moc"