feat: 添加GPU频率/功耗/风扇监控、温度告警、趋势图、右键菜单、进程列表、GPU切换

第二、三阶段功能:

数据增强(第一阶段):
- GPU频率: NVIDIA nvidia-smi clocks.gr, Intel gt/gt0/rps_act_freq_mhz
- 显存频率: NVIDIA nvidia-smi clocks.mem
- 功耗: NVIDIA nvidia-smi power.draw, AMD hwmon power1_average
- 风扇转速: NVIDIA nvidia-smi fan.speed, AMD hwmon fan1_input
- 任务栏图标根据最高温度变色(≥80°C红/≥60°C橙)
- readGpuStats 每次刷新前重置统计字段,避免残留旧值

交互提升(第二阶段):
- 温度告警: GPU温度≥80°C时通过D-Bus发送桌面通知,30秒冷却
- 右键菜单: 使用Qt.labs.platform原生菜单+MenuHelper,支持刷新和GPU切换
- 迷你趋势图: Canvas绘制近60秒温度走势折线
- 英文翻译: 新增en.ts,23条全部翻译

高级功能(第三阶段):
- NVIDIA进程列表: nvidia-smi --query-compute-apps查询,弹框中显示进程名和显存占用
- GPU切换: pkexec prime-select set <mode>,弹框按钮+右键菜单,仅PRIME模式显示

其他改进:
- 自定义深色主题tooltip替代系统默认白色tooltip
- 第一张卡片tooltip显示在下方避免被裁剪
- CMakeLists.txt lupdate添加C++源文件提取tr()翻译
- gpuStats格式扩展为9字段: temp|gpuUsage|memUsage|memUsed|memTotal|gpuClock|memClock|powerDraw|fanSpeed
This commit is contained in:
2026-07-14 17:32:46 +08:00
parent cda61d9d72
commit 2570a028ea
7 changed files with 819 additions and 45 deletions
+178 -6
View File
@@ -12,6 +12,10 @@
#include <QTextStream>
#include <QRegularExpression>
#include <QFileInfo>
#include <QDateTime>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QVariantList>
DCORE_USE_NAMESPACE
@@ -20,6 +24,7 @@ DS_BEGIN_NAMESPACE
GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent)
: DApplet(parent)
, m_ready(false)
, m_lastAlertTime(0)
{
}
@@ -73,6 +78,24 @@ QStringList GraphicsDriverApplet::gpuPrimary() const
return m_gpuPrimary;
}
QVariantList GraphicsDriverApplet::tempHistory() const
{
QVariantList result;
for (const QList<int> &history : m_tempHistory) {
QVariantList gpuHistory;
for (int temp : history) {
gpuHistory << temp;
}
result << QVariant(gpuHistory);
}
return result;
}
QStringList GraphicsDriverApplet::gpuProcesses() const
{
return m_gpuProcesses;
}
void GraphicsDriverApplet::refreshDeviceInfo()
{
detectGpus();
@@ -86,6 +109,7 @@ void GraphicsDriverApplet::refreshStats()
void GraphicsDriverApplet::detectGpus()
{
m_gpus.clear();
m_tempHistory.clear();
// 通过 lspci 检测显卡
QStringList lines = runCommand("lspci", {"-mm"});
@@ -103,6 +127,10 @@ void GraphicsDriverApplet::detectGpus()
gpu.memUsage = -1;
gpu.memTotal = -1;
gpu.memUsed = -1;
gpu.gpuClock = -1;
gpu.memClock = -1;
gpu.powerDraw = -1;
gpu.fanSpeed = -1;
// lspci -mm 格式: "Class" "Vendor" "Device" ...
// 提取 PCI ID(行首的 01:00.0 等)
@@ -326,10 +354,11 @@ int GraphicsDriverApplet::findDrmCard(const QString &pciId)
void GraphicsDriverApplet::readGpuStats()
{
// 先处理 NVIDIA GPU(单次 nvidia-smi 调用获取所有 NVIDIA GPU 数据)
// nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used
// nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,
// memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed
QMap<QString, QStringList> nvidiaData; // pciId -> stats
QStringList nvLines = runCommand("nvidia-smi", {
"--query-gpu=pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used",
"--query-gpu=pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed",
"--format=csv,noheader,nounits"
});
for (const QString &line : nvLines) {
@@ -344,6 +373,17 @@ void GraphicsDriverApplet::readGpuStats()
// 逐个 GPU 读取统计
for (GpuInfo &gpu : m_gpus) {
// 每次刷新前重置实时统计字段,确保数据源不可用时不会残留旧值
gpu.temperature = -1;
gpu.gpuUsage = -1;
gpu.memUsage = -1;
gpu.memTotal = -1;
gpu.memUsed = -1;
gpu.gpuClock = -1;
gpu.memClock = -1;
gpu.powerDraw = -1;
gpu.fanSpeed = -1;
if (gpu.driver == "nvidia" && nvidiaData.contains(gpu.pciId)) {
// NVIDIA: 使用 nvidia-smi 数据
QStringList &parts = nvidiaData[gpu.pciId];
@@ -352,6 +392,19 @@ void GraphicsDriverApplet::readGpuStats()
gpu.memUsage = parts[3].trimmed().toInt();
gpu.memTotal = parts[4].trimmed().toInt();
gpu.memUsed = parts[5].trimmed().toInt();
// 频率 (parts[6]=GPU, parts[7]=显存)
if (parts.size() > 6) gpu.gpuClock = parts[6].trimmed().toInt();
if (parts.size() > 7) gpu.memClock = parts[7].trimmed().toInt();
// 功耗 (parts[8])[N/A] 时保持 -1
if (parts.size() > 8) {
QString pwr = parts[8].trimmed();
if (pwr != "[N/A]" && !pwr.isEmpty()) gpu.powerDraw = pwr.toDouble();
}
// 风扇转速 (parts[9])[N/A] 时保持 -1
if (parts.size() > 9) {
QString fan = parts[9].trimmed();
if (fan != "[N/A]" && !fan.isEmpty()) gpu.fanSpeed = fan.toInt();
}
} else if (gpu.drmCard >= 0) {
// AMD / Intel / 其他: 使用 sysfs
QString cardPath = QString("/sys/class/drm/card%1/device").arg(gpu.drmCard);
@@ -384,16 +437,36 @@ void GraphicsDriverApplet::readGpuStats()
if (nameFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString name = QTextStream(&nameFile).readAll().trimmed();
if (name == gpu.driver || name == "amdgpu" || name == "radeon") {
QFile tempFile(hwmonDir.absoluteFilePath(hwmon + "/temp1_input"));
QString hwmonPath = hwmonDir.absoluteFilePath(hwmon);
// 温度
QFile tempFile(hwmonPath + "/temp1_input");
if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt();
gpu.temperature = tempMilli / 1000;
}
// 功耗 (µW -> W)
QFile powerFile(hwmonPath + "/power1_average");
if (powerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qint64 microWatts = QTextStream(&powerFile).readAll().trimmed().toLongLong();
gpu.powerDraw = microWatts / 1000000.0;
}
// 风扇转速 (RPM)
QFile fanFile(hwmonPath + "/fan1_input");
if (fanFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.fanSpeed = QTextStream(&fanFile).readAll().trimmed().toInt();
}
break;
}
}
}
}
// GPU 频率: Intel i915 使用 gt/gt0/rps_act_freq_mhzAMD 使用 hwmon freq1_input
QString gtPath = QString("/sys/class/drm/card%1/gt/gt0/rps_act_freq_mhz").arg(gpu.drmCard);
QFile gtFreqFile(gtPath);
if (gtFreqFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.gpuClock = QTextStream(&gtFreqFile).readAll().trimmed().toInt();
}
}
// --- 以下为 Intel 核显等无 hwmon/vram sysfs 接口时的回退逻辑 ---
@@ -443,18 +516,117 @@ void GraphicsDriverApplet::readGpuStats()
}
}
// 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal)
// 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal|gpuClock|memClock|powerDraw|fanSpeed)
m_gpuStats.clear();
for (const GpuInfo &gpu : m_gpus) {
m_gpuStats << QString("%1|%2|%3|%4|%5")
m_gpuStats << QString("%1|%2|%3|%4|%5|%6|%7|%8|%9")
.arg(gpu.temperature)
.arg(gpu.gpuUsage)
.arg(gpu.memUsage)
.arg(gpu.memUsed)
.arg(gpu.memTotal);
.arg(gpu.memTotal)
.arg(gpu.gpuClock)
.arg(gpu.memClock)
.arg(gpu.powerDraw, 0, 'f', 1)
.arg(gpu.fanSpeed);
}
emit gpuStatsChanged();
// 更新温度历史(保留最近 60 个采样点)
if (m_tempHistory.size() != m_gpus.size()) {
m_tempHistory.resize(m_gpus.size());
}
for (int i = 0; i < m_gpus.size(); ++i) {
m_tempHistory[i].append(m_gpus[i].temperature);
if (m_tempHistory[i].size() > 60) {
m_tempHistory[i].removeFirst();
}
}
emit tempHistoryChanged();
// 温度告警检测
checkTempAlert();
// 读取 NVIDIA 进程列表
readGpuProcesses();
}
// 温度告警:检测是否有 GPU 温度超过 80°C,30 秒内不重复告警
void GraphicsDriverApplet::checkTempAlert()
{
qint64 now = QDateTime::currentSecsSinceEpoch();
if (now - m_lastAlertTime < 30) return;
for (const GpuInfo &gpu : m_gpus) {
if (gpu.temperature >= 80) {
sendNotification(
tr("GPU Temperature Alert"),
tr("%1 temperature reached %2°C, please check cooling")
.arg(gpu.name)
.arg(gpu.temperature)
);
m_lastAlertTime = now;
break;
}
}
}
// 通过 D-Bus 发送桌面通知
void GraphicsDriverApplet::sendNotification(const QString &summary, const QString &body)
{
QDBusInterface notify("org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
QDBusConnection::sessionBus());
if (notify.isValid()) {
notify.call("Notify", "dde-shell-graphics-driver", 0u,
"dialog-warning", summary, body,
QStringList(), QVariantMap(), 5000);
}
}
// 读取 NVIDIA GPU 上运行的进程列表
void GraphicsDriverApplet::readGpuProcesses()
{
m_gpuProcesses.clear();
// 仅当存在 NVIDIA GPU 时查询
bool hasNvidia = false;
for (const GpuInfo &gpu : m_gpus) {
if (gpu.driver == "nvidia") { hasNvidia = true; break; }
}
if (!hasNvidia) {
emit gpuProcessesChanged();
return;
}
// nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader,nounits
QStringList lines = runCommand("nvidia-smi", {
"--query-compute-apps=pid,process_name,used_memory",
"--format=csv,noheader,nounits"
});
for (const QString &line : lines) {
QStringList parts = line.split(",");
if (parts.size() >= 3) {
m_gpuProcesses << QString("%1|%2|%3")
.arg(parts[0].trimmed())
.arg(parts[1].trimmed())
.arg(parts[2].trimmed());
}
}
emit gpuProcessesChanged();
}
// GPU 切换:调用 prime-select 切换 PRIME 模式(需要 polkit 提权)
bool GraphicsDriverApplet::switchGpu(const QString &mode)
{
// mode: "nvidia" / "intel" / "on-demand"
QProcess process;
process.start("pkexec", {"prime-select", "set", mode});
process.waitForFinished(30000);
return process.exitCode() == 0;
}
D_APPLET_CLASS(GraphicsDriverApplet)