feat: 添加GPU温度/使用率/显存实时监控

- C++后端:GpuInfo扩展统计字段,readGpuStats()支持NVIDIA(nvidia-smi)和AMD(sysfs)
- PCI到DRM card映射,新增gpuStats属性和refreshStats方法
- QML前端:弹窗卡片显示温度(带颜色)、GPU使用率进度条、显存使用
- 任务栏tooltip显示简洁摘要(厂商简称+温度+使用率)
- 定时刷新:弹窗和tooltip打开时每2秒刷新,关闭时停止
- 优化卡片布局:压缩高度和间距,弹窗高度调整至420px
This commit is contained in:
2026-07-13 23:08:08 +08:00
parent 88e03f14d2
commit 403ae8a69b
3 changed files with 336 additions and 31 deletions
+132
View File
@@ -11,6 +11,7 @@
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QFileInfo>
DCORE_USE_NAMESPACE
@@ -57,11 +58,21 @@ bool GraphicsDriverApplet::ready() const
return m_ready;
}
QStringList GraphicsDriverApplet::gpuStats() const
{
return m_gpuStats;
}
void GraphicsDriverApplet::refreshDeviceInfo()
{
detectGpus();
}
void GraphicsDriverApplet::refreshStats()
{
readGpuStats();
}
void GraphicsDriverApplet::detectGpus()
{
m_gpus.clear();
@@ -74,6 +85,14 @@ void GraphicsDriverApplet::detectGpus()
line.contains("Display controller", Qt::CaseInsensitive)) {
GpuInfo gpu;
// 初始化统计字段
gpu.drmCard = -1;
gpu.temperature = -1;
gpu.gpuUsage = -1;
gpu.memUsage = -1;
gpu.memTotal = -1;
gpu.memUsed = -1;
// lspci -mm 格式: "Class" "Vendor" "Device" ...
// 提取 PCI ID(行首的 01:00.0 等)
int spaceIdx = line.indexOf(' ');
@@ -97,6 +116,9 @@ void GraphicsDriverApplet::detectGpus()
gpu.driver = "unknown";
}
// 查找 DRM 卡号
gpu.drmCard = findDrmCard(gpu.pciId);
m_gpus.append(gpu);
}
}
@@ -131,6 +153,9 @@ void GraphicsDriverApplet::detectGpus()
emit currentDriverChanged();
emit gpuSummaryChanged();
emit readyChanged();
// 读取实时统计信息
readGpuStats();
}
QStringList GraphicsDriverApplet::runCommand(const QString &cmd, const QStringList &args)
@@ -213,6 +238,113 @@ QString GraphicsDriverApplet::readDriverVersion(const QString &driverName)
return QString();
}
int GraphicsDriverApplet::findDrmCard(const QString &pciId)
{
// 扫描 /sys/bus/pci/devices/0000:XX:XX.X/drm/ 目录,找到 cardN 条目
QString drmPath = "/sys/bus/pci/devices/0000:" + pciId + "/drm";
QDir dir(drmPath);
if (!dir.exists()) return -1;
QStringList entries = dir.entryList(QStringList() << "card*", QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
// 匹配 "card0" "card1" 等,排除 "card0-DP-1" 等连接器
QRegularExpression re("^card(\\d+)$");
auto match = re.match(entry);
if (match.hasMatch()) {
return match.captured(1).toInt();
}
}
return -1;
}
void GraphicsDriverApplet::readGpuStats()
{
// 先处理 NVIDIA GPU(单次 nvidia-smi 调用获取所有 NVIDIA GPU 数据)
// nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used
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",
"--format=csv,noheader,nounits"
});
for (const QString &line : nvLines) {
QStringList parts = line.split(",");
if (parts.size() >= 6) {
// nvidia-smi pci.bus_id 格式: 00000000:01:00.0,提取后半部分匹配
QString busId = parts[0].trimmed();
QString shortId = busId.section(':', -2); // "01:00.0"
nvidiaData[shortId] = parts;
}
}
// 逐个 GPU 读取统计
for (GpuInfo &gpu : m_gpus) {
if (gpu.driver == "nvidia" && nvidiaData.contains(gpu.pciId)) {
// NVIDIA: 使用 nvidia-smi 数据
QStringList &parts = nvidiaData[gpu.pciId];
gpu.temperature = parts[1].trimmed().toInt();
gpu.gpuUsage = parts[2].trimmed().toInt();
gpu.memUsage = parts[3].trimmed().toInt();
gpu.memTotal = parts[4].trimmed().toInt();
gpu.memUsed = parts[5].trimmed().toInt();
} else if (gpu.drmCard >= 0) {
// AMD / Intel / 其他: 使用 sysfs
QString cardPath = QString("/sys/class/drm/card%1/device").arg(gpu.drmCard);
// GPU 使用率
QFile busyFile(cardPath + "/gpu_busy_percent");
if (busyFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.gpuUsage = QTextStream(&busyFile).readAll().trimmed().toInt();
}
// 显存 (字节,转换为 MB)
QFile vramTotal(cardPath + "/mem_info_vram_total");
QFile vramUsed(cardPath + "/mem_info_vram_used");
if (vramTotal.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.memTotal = QTextStream(&vramTotal).readAll().trimmed().toLongLong() / 1024 / 1024;
}
if (vramUsed.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.memUsed = QTextStream(&vramUsed).readAll().trimmed().toLongLong() / 1024 / 1024;
}
if (gpu.memTotal > 0 && gpu.memUsed >= 0) {
gpu.memUsage = gpu.memUsed * 100 / gpu.memTotal;
}
// 温度: 查找 hwmon 子目录
QDir hwmonDir(cardPath + "/hwmon");
if (hwmonDir.exists()) {
QStringList hwmons = hwmonDir.entryList(QStringList() << "hwmon*", QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &hwmon : hwmons) {
QFile nameFile(hwmonDir.absoluteFilePath(hwmon + "/name"));
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"));
if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt();
gpu.temperature = tempMilli / 1000;
}
break;
}
}
}
}
}
}
// 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal)
m_gpuStats.clear();
for (const GpuInfo &gpu : m_gpus) {
m_gpuStats << QString("%1|%2|%3|%4|%5")
.arg(gpu.temperature)
.arg(gpu.gpuUsage)
.arg(gpu.memUsage)
.arg(gpu.memUsed)
.arg(gpu.memTotal);
}
emit gpuStatsChanged();
}
D_APPLET_CLASS(GraphicsDriverApplet)
DS_END_NAMESPACE