initial commit
This commit is contained in:
commit
222fae10c7
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build
|
||||
.vscode
|
||||
64
CMakeLists.txt
Normal file
64
CMakeLists.txt
Normal file
@ -0,0 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(nvme-monitor)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED gtk4)
|
||||
pkg_check_modules(CAIRO REQUIRED cairo)
|
||||
|
||||
# Find glib-compile-resources tool
|
||||
find_program(GLIB_COMPILE_RESOURCES glib-compile-resources REQUIRED)
|
||||
|
||||
include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||
include_directories(${CMAKE_BINARY_DIR})
|
||||
include_directories(${GTK_INCLUDE_DIRS})
|
||||
include_directories(${CAIRO_INCLUDE_DIRS})
|
||||
|
||||
# Compile resources
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/resources.c
|
||||
COMMAND ${GLIB_COMPILE_RESOURCES}
|
||||
--generate-source
|
||||
--sourcedir=${CMAKE_SOURCE_DIR}/resources
|
||||
--target=${CMAKE_BINARY_DIR}/resources.c
|
||||
${CMAKE_SOURCE_DIR}/resources/resources.xml
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/resources/nvme-monitor.svg
|
||||
${CMAKE_SOURCE_DIR}/resources/resources.xml
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/mainwindow.cpp
|
||||
src/nvme_monitor.cpp
|
||||
src/temperature_chart.cpp
|
||||
src/config_manager.cpp
|
||||
${CMAKE_BINARY_DIR}/resources.c
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
include/mainwindow.h
|
||||
include/nvme_monitor.h
|
||||
include/temperature_chart.h
|
||||
include/config_manager.h
|
||||
)
|
||||
|
||||
add_executable(nvme-monitor ${SOURCES} ${HEADERS})
|
||||
|
||||
target_link_libraries(nvme-monitor
|
||||
${GTK_LIBRARIES}
|
||||
${CAIRO_LIBRARIES}
|
||||
m
|
||||
)
|
||||
|
||||
target_compile_options(nvme-monitor PRIVATE ${GTK_CFLAGS_OTHER})
|
||||
target_compile_options(nvme-monitor PRIVATE ${CAIRO_CFLAGS_OTHER})
|
||||
|
||||
# Install executable
|
||||
install(TARGETS nvme-monitor
|
||||
DESTINATION bin)
|
||||
|
||||
# Install desktop file for system integration
|
||||
install(FILES resources/nvme-monitor.desktop
|
||||
DESTINATION share/applications)
|
||||
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# NVMe Temperature Monitor
|
||||
|
||||
Jednoduchá GUI aplikace v C++, která v reálném čase zobrazuje teploty NVMe disků.
|
||||
|
||||
## Požadavky
|
||||
|
||||
- GTK 4 development balíčky
|
||||
- Cairo development balíčky
|
||||
- CMake >= 3.16
|
||||
- GCC/Clang kompilátor s C++17
|
||||
- `nvme-cli` balíček nainstalovaný
|
||||
|
||||
### Instalace závislostí (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
sudo apt-get install libgtk-4-dev libcairo2-dev cmake build-essential nvme-cli
|
||||
```
|
||||
|
||||
### Instalace závislostí (Fedora/RHEL)
|
||||
|
||||
```bash
|
||||
sudo dnf install gtk4-devel cairo-devel cmake gcc-c++ nvme-cli
|
||||
```
|
||||
|
||||
### Instalace závislostí (Arch)
|
||||
|
||||
```bash
|
||||
sudo pacman -S gtk4 cairo cmake base-devel nvme-cli
|
||||
```
|
||||
|
||||
## Kompilace
|
||||
|
||||
```bash
|
||||
cd /home/kamma/projects/nvme-monitor
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Spuštění
|
||||
|
||||
```bash
|
||||
sudo ./build/nvme-monitor
|
||||
```
|
||||
|
||||
**Poznámka:** Aplikace vyžaduje sudo oprávnění pro čtení informací z NVMe zařízení.
|
||||
|
||||
## Funkce
|
||||
|
||||
- ✅ Detekce všech NVMe disků
|
||||
- ✅ Čtení teplot z hlavního senzoru a dalších senzorů
|
||||
- ✅ Reálný čas graf s více řadami (každý disk v jiné barvě)
|
||||
- ✅ Nastavitelný interval obnovení (100-10000 ms)
|
||||
- ✅ Tlačítko pro vymazání grafu
|
||||
- ✅ Status bar s informacemi o počtu zařízení a odečtech
|
||||
- ✅ Cairo kreslení pro hladké grafy
|
||||
- ✅ Mřížka a popisky os
|
||||
|
||||
## Struktura projektu
|
||||
|
||||
```
|
||||
nvme-monitor/
|
||||
├── CMakeLists.txt # Konfigurace CMake
|
||||
├── src/
|
||||
│ ├── main.cpp # Vstupní bod aplikace
|
||||
│ ├── mainwindow.cpp # Hlavní okno UI (GTK)
|
||||
│ ├── nvme_monitor.cpp # Logika čtení NVME dat
|
||||
│ └── temperature_chart.cpp # Komponenta grafu (cairo)
|
||||
├── include/
|
||||
│ ├── mainwindow.h
|
||||
│ ├── nvme_monitor.h
|
||||
│ └── temperature_chart.h
|
||||
└── build.sh # Build skript
|
||||
```
|
||||
|
||||
## Použití
|
||||
|
||||
1. Spusťte aplikaci se sudo: `sudo ./build/nvme-monitor`
|
||||
2. Okno se otevře a začne automaticky číst teploty
|
||||
3. Používejte spinner pro změnu intervalu obnovení
|
||||
4. Stiskněte "Clear Data" pro vymazání grafu
|
||||
5. Stiskněte "Quit" pro ukončení aplikace
|
||||
|
||||
## Poznámky
|
||||
|
||||
- Aplikace parsuje výstup z `nvme smart-log` příkazů
|
||||
- Podporuje více NVME zařízení, každé v jiné barvě
|
||||
- Parsuje jak hlavní teplotu, tak dodatečné senzory teploty
|
||||
- Graf drží posledních 300 datových bodů pro lepší výkon
|
||||
- Kreslení grafů je implementováno s Cairo pro lepší flexibilitu
|
||||
- Mřížka se automaticky škáluje podle rozsahu teplot
|
||||
3. Používejte spinner pro změnu intervalu obnovení
|
||||
4. Stiskněte "Clear Data" pro vymazání grafu
|
||||
|
||||
## Poznamky
|
||||
|
||||
- Aplikace parsuje výstup z `nvme smart-log` příkazu
|
||||
- Podporuje více NVME zařízení, každé v jiné barvě
|
||||
- Parsuje jak hlavní teplotu, tak dodatečné senzory teploty
|
||||
- Graph drží posledních 300 datových bodů pro lepší výkon
|
||||
11
build.sh
Executable file
11
build.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create build directory
|
||||
mkdir -p build
|
||||
cd build
|
||||
|
||||
# Build project
|
||||
cmake ..
|
||||
make
|
||||
|
||||
echo "Build complete! Run with: ./nvme-monitor"
|
||||
33
include/config_manager.h
Normal file
33
include/config_manager.h
Normal file
@ -0,0 +1,33 @@
|
||||
#ifndef CONFIG_MANAGER_H
|
||||
#define CONFIG_MANAGER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
ConfigManager();
|
||||
|
||||
// Load configuration
|
||||
void load();
|
||||
|
||||
// Save configuration
|
||||
void save();
|
||||
|
||||
// Getters and setters
|
||||
int getWindowWidth() const { return windowWidth; }
|
||||
int getWindowHeight() const { return windowHeight; }
|
||||
int getPollingTime() const { return pollingTime; }
|
||||
|
||||
void setWindowWidth(int w) { windowWidth = w; }
|
||||
void setWindowHeight(int h) { windowHeight = h; }
|
||||
void setPollingTime(int t) { pollingTime = t; }
|
||||
|
||||
private:
|
||||
std::string getConfigFilePath() const;
|
||||
|
||||
int windowWidth;
|
||||
int windowHeight;
|
||||
int pollingTime;
|
||||
};
|
||||
|
||||
#endif // CONFIG_MANAGER_H
|
||||
42
include/mainwindow.h
Normal file
42
include/mainwindow.h
Normal file
@ -0,0 +1,42 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include "nvme_monitor.h"
|
||||
#include "temperature_chart.h"
|
||||
#include "config_manager.h"
|
||||
|
||||
class MainWindow {
|
||||
public:
|
||||
MainWindow();
|
||||
~MainWindow();
|
||||
|
||||
void show();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void updateTemperatures();
|
||||
void updateLegend();
|
||||
void saveWindowState();
|
||||
|
||||
static gboolean onDeleteWindow(GtkWidget *widget, gpointer userData);
|
||||
static gboolean onUpdateTimer(gpointer userData);
|
||||
static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData);
|
||||
static void onClearButtonClicked(GtkButton *button, gpointer userData);
|
||||
static void onQuitButtonClicked(GtkButton *button, gpointer userData);
|
||||
|
||||
GtkWidget *window;
|
||||
GtkWidget *statusLabel;
|
||||
GtkSpinButton *refreshRateSpinBox;
|
||||
TemperatureChart *chart;
|
||||
NvmeMonitor *monitor;
|
||||
ConfigManager *config;
|
||||
guint timerID;
|
||||
int refreshRateSec;
|
||||
GtkWidget *legendBox;
|
||||
};
|
||||
|
||||
// Global main loop reference for proper shutdown
|
||||
extern GMainLoop *gMainLoop;
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
34
include/nvme_monitor.h
Normal file
34
include/nvme_monitor.h
Normal file
@ -0,0 +1,34 @@
|
||||
#ifndef NVME_MONITOR_H
|
||||
#define NVME_MONITOR_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
struct TemperatureData {
|
||||
double temperature;
|
||||
std::string device;
|
||||
std::string sensorName;
|
||||
long timestamp;
|
||||
};
|
||||
|
||||
class NvmeMonitor {
|
||||
public:
|
||||
explicit NvmeMonitor();
|
||||
|
||||
// Get list of available NVMe devices
|
||||
std::vector<std::string> getAvailableDevices();
|
||||
|
||||
// Read temperatures from a specific device
|
||||
std::map<std::string, double> readTemperatures(const std::string &device);
|
||||
|
||||
// Get all available devices and their temperatures
|
||||
std::map<std::string, std::map<std::string, double>> getAllTemperatures();
|
||||
|
||||
private:
|
||||
std::vector<std::string> scanDevices();
|
||||
std::map<std::string, std::string> findHwmonDevices();
|
||||
double readTemperatureFromFile(const std::string &filePath);
|
||||
};
|
||||
|
||||
#endif // NVME_MONITOR_H
|
||||
79
include/temperature_chart.h
Normal file
79
include/temperature_chart.h
Normal file
@ -0,0 +1,79 @@
|
||||
#ifndef TEMPERATURE_CHART_H
|
||||
#define TEMPERATURE_CHART_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <cairo.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
|
||||
struct DataPoint {
|
||||
double temperature;
|
||||
int64_t timestamp;
|
||||
};
|
||||
|
||||
struct SeriesData {
|
||||
std::vector<DataPoint> points;
|
||||
GdkRGBA color;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class TemperatureChart {
|
||||
public:
|
||||
TemperatureChart(GtkWidget *parent = nullptr);
|
||||
~TemperatureChart();
|
||||
|
||||
GtkWidget* getWidget() const { return drawingArea; }
|
||||
|
||||
void addTemperatureData(const std::string &device, const std::string &sensor,
|
||||
double temperature, int64_t timestamp);
|
||||
void clear();
|
||||
void draw();
|
||||
|
||||
// Get list of devices with their colors
|
||||
std::vector<std::pair<std::string, GdkRGBA>> getDeviceColors() const;
|
||||
void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height);
|
||||
|
||||
private:
|
||||
void setupColors();
|
||||
GdkRGBA getColorForDevice(const std::string &device);
|
||||
void redraw();
|
||||
void updateThemeColors();
|
||||
|
||||
static gboolean onTick(gpointer userData);
|
||||
static void onLeave(GtkEventControllerMotion *motion, gpointer userData);
|
||||
|
||||
struct NearestPoint {
|
||||
bool found;
|
||||
double temperature;
|
||||
int64_t timestamp;
|
||||
std::string seriesName;
|
||||
double distance;
|
||||
};
|
||||
|
||||
NearestPoint findNearestDataPoint(double mouseX, double mouseY, int width, int height);
|
||||
void showTooltip(double x, double y, const NearestPoint &point);
|
||||
void hideTooltip();
|
||||
|
||||
GtkWidget *drawingArea;
|
||||
GtkWidget *tooltipWindow;
|
||||
GtkWidget *tooltipLabel;
|
||||
std::map<std::string, SeriesData> seriesMap;
|
||||
std::map<std::string, GdkRGBA> colorMap;
|
||||
int maxDataPoints;
|
||||
guint tickHandler;
|
||||
double lastMouseX, lastMouseY;
|
||||
|
||||
// Cairo drawing state
|
||||
double minTemp, maxTemp;
|
||||
int64_t minTime, maxTime;
|
||||
static constexpr int MAX_TIME_MS = 600000; // 10 minutes in milliseconds
|
||||
static constexpr double MIN_TEMP = 10.0;
|
||||
static constexpr double MAX_TEMP = 110.0;
|
||||
|
||||
// Theme colors
|
||||
GdkRGBA bgColor, textColor, gridColor, axisColor;
|
||||
};
|
||||
|
||||
#endif // TEMPERATURE_CHART_H
|
||||
21
install.sh
Normal file
21
install.sh
Normal file
@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Install script for nvme-monitor
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
echo "Building nvme-monitor..."
|
||||
./build.sh
|
||||
|
||||
if [ ! -f build/nvme-monitor ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing nvme-monitor..."
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
|
||||
echo "Installation complete!"
|
||||
echo "Application icon is embedded in the executable"
|
||||
echo "Run 'nvme-monitor' to start the application"
|
||||
|
||||
9
resources/nvme-monitor.desktop
Normal file
9
resources/nvme-monitor.desktop
Normal file
@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=NVMe Monitor
|
||||
Comment=Monitor NVMe drive temperatures
|
||||
Exec=nvme-monitor
|
||||
Icon=nvme-monitor
|
||||
Categories=Utility;System;
|
||||
Terminal=false
|
||||
47
resources/nvme-monitor.svg
Normal file
47
resources/nvme-monitor.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FF6B6B;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FF0000;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background circle -->
|
||||
<circle cx="128" cy="128" r="120" fill="#1a1a1a" stroke="#333" stroke-width="2"/>
|
||||
|
||||
<!-- Thermometer bulb -->
|
||||
<circle cx="128" cy="200" r="25" fill="url(#grad1)" stroke="#cc0000" stroke-width="2"/>
|
||||
|
||||
<!-- Thermometer tube -->
|
||||
<rect x="118" y="80" width="20" height="120" fill="#f5f5f5" stroke="#333" stroke-width="2" rx="10"/>
|
||||
|
||||
<!-- Mercury level (70%) -->
|
||||
<rect x="122" y="115" width="12" height="85" fill="#ff4444" rx="6"/>
|
||||
|
||||
<!-- Scale marks -->
|
||||
<!-- Top mark (110°C) -->
|
||||
<line x1="110" y1="85" x2="100" y2="85" stroke="#fff" stroke-width="2"/>
|
||||
<text x="95" y="90" font-size="10" fill="#fff" text-anchor="end">110</text>
|
||||
|
||||
<!-- Upper mark (80°C) -->
|
||||
<line x1="110" y1="120" x2="100" y2="120" stroke="#fff" stroke-width="2"/>
|
||||
<text x="95" y="125" font-size="10" fill="#fff" text-anchor="end">80</text>
|
||||
|
||||
<!-- Middle mark (50°C) -->
|
||||
<line x1="110" y1="155" x2="100" y2="155" stroke="#fff" stroke-width="2"/>
|
||||
<text x="95" y="160" font-size="10" fill="#fff" text-anchor="end">50</text>
|
||||
|
||||
<!-- Lower mark (20°C) -->
|
||||
<line x1="110" y1="190" x2="100" y2="190" stroke="#fff" stroke-width="2"/>
|
||||
<text x="95" y="195" font-size="10" fill="#fff" text-anchor="end">20</text>
|
||||
|
||||
<!-- Right side marks -->
|
||||
<line x1="146" y1="85" x2="156" y2="85" stroke="#fff" stroke-width="2"/>
|
||||
<line x1="146" y1="120" x2="156" y2="120" stroke="#fff" stroke-width="2"/>
|
||||
<line x1="146" y1="155" x2="156" y2="155" stroke="#fff" stroke-width="2"/>
|
||||
<line x1="146" y1="190" x2="156" y2="190" stroke="#fff" stroke-width="2"/>
|
||||
|
||||
<!-- NVMe text -->
|
||||
<text x="128" y="240" font-size="16" font-weight="bold" fill="#ff6b6b" text-anchor="middle">NVMe</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
7
resources/resources.xml
Normal file
7
resources/resources.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/kamma/nvme-monitor">
|
||||
<file>nvme-monitor.svg</file>
|
||||
<file>style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
4
resources/style.css
Normal file
4
resources/style.css
Normal file
@ -0,0 +1,4 @@
|
||||
/* Default application styles and icon */
|
||||
window {
|
||||
/* Icon can be set via CSS if needed */
|
||||
}
|
||||
94
src/config_manager.cpp
Normal file
94
src/config_manager.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
#include "config_manager.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <sys/stat.h>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
ConfigManager::ConfigManager()
|
||||
: windowWidth(1200), windowHeight(700), pollingTime(3)
|
||||
{
|
||||
}
|
||||
|
||||
std::string ConfigManager::getConfigFilePath() const
|
||||
{
|
||||
// Get the directory of the executable using /proc/self/exe
|
||||
char path[1024];
|
||||
ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
|
||||
if (len == -1) {
|
||||
// Fallback to current directory
|
||||
return "./nvme-monitor.conf";
|
||||
}
|
||||
|
||||
path[len] = '\0';
|
||||
|
||||
// Get directory from full path
|
||||
std::string fullPath(path);
|
||||
size_t lastSlash = fullPath.find_last_of('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
std::string exeDir = fullPath.substr(0, lastSlash);
|
||||
return exeDir + "/nvme-monitor.conf";
|
||||
}
|
||||
|
||||
return "./nvme-monitor.conf";
|
||||
}
|
||||
|
||||
void ConfigManager::load()
|
||||
{
|
||||
std::string configPath = getConfigFilePath();
|
||||
std::ifstream file(configPath);
|
||||
|
||||
if (!file.is_open()) {
|
||||
// Config file doesn't exist, use defaults
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
// Skip empty lines and comments
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
size_t pos = line.find('=');
|
||||
if (pos == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, pos);
|
||||
std::string value = line.substr(pos + 1);
|
||||
|
||||
// Trim whitespace
|
||||
key.erase(key.find_last_not_of(" \t") + 1);
|
||||
value.erase(0, value.find_first_not_of(" \t"));
|
||||
value.erase(value.find_last_not_of(" \t") + 1);
|
||||
|
||||
if (key == "window_width") {
|
||||
windowWidth = std::stoi(value);
|
||||
} else if (key == "window_height") {
|
||||
windowHeight = std::stoi(value);
|
||||
} else if (key == "polling_time") {
|
||||
pollingTime = std::stoi(value);
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
void ConfigManager::save()
|
||||
{
|
||||
std::string configPath = getConfigFilePath();
|
||||
|
||||
std::ofstream file(configPath);
|
||||
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to save config to: " << configPath << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
file << "# NVMe Monitor Configuration\n";
|
||||
file << "# Auto-generated, do not edit manually\n\n";
|
||||
file << "window_width = " << windowWidth << "\n";
|
||||
file << "window_height = " << windowHeight << "\n";
|
||||
file << "polling_time = " << pollingTime << "\n";
|
||||
|
||||
file.close();
|
||||
}
|
||||
65
src/main.cpp
Normal file
65
src/main.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <glib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include "mainwindow.h"
|
||||
|
||||
// Declaration from resources.c
|
||||
extern "C" {
|
||||
GResource *resources_get_resource();
|
||||
}
|
||||
|
||||
GMainLoop *gMainLoop = nullptr;
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// Daemonize the process
|
||||
pid_t pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
return 1; // Fork failed
|
||||
}
|
||||
|
||||
if (pid > 0) {
|
||||
return 0; // Parent process exits
|
||||
}
|
||||
|
||||
// Child process continues as daemon
|
||||
umask(0);
|
||||
|
||||
// Create new session
|
||||
if (setsid() < 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Change directory to root
|
||||
chdir("/");
|
||||
|
||||
// Close standard file descriptors
|
||||
int fd = open("/dev/null", O_RDWR);
|
||||
if (fd != -1) {
|
||||
dup2(fd, STDIN_FILENO);
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
dup2(fd, STDERR_FILENO);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
gtk_init();
|
||||
|
||||
// Register resources containing the application icon
|
||||
GResource *resource = resources_get_resource();
|
||||
g_resources_register(resource);
|
||||
|
||||
gMainLoop = g_main_loop_new(nullptr, FALSE);
|
||||
|
||||
MainWindow *window = new MainWindow();
|
||||
window->show();
|
||||
|
||||
// Run the main event loop
|
||||
g_main_loop_run(gMainLoop);
|
||||
g_main_loop_unref(gMainLoop);
|
||||
|
||||
return 0;
|
||||
}
|
||||
320
src/mainwindow.cpp
Normal file
320
src/mainwindow.cpp
Normal file
@ -0,0 +1,320 @@
|
||||
#include "mainwindow.h"
|
||||
#include <iostream>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <gio/gio.h>
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
|
||||
MainWindow::MainWindow()
|
||||
: window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr),
|
||||
chart(nullptr), monitor(nullptr), config(nullptr), timerID(0), refreshRateSec(3)
|
||||
{
|
||||
monitor = new NvmeMonitor();
|
||||
config = new ConfigManager();
|
||||
config->load();
|
||||
|
||||
refreshRateSec = config->getPollingTime();
|
||||
|
||||
setupUI();
|
||||
|
||||
// Set window size from config
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), config->getWindowWidth(), config->getWindowHeight());
|
||||
|
||||
// Start update timer
|
||||
timerID = g_timeout_add(refreshRateSec * 1000, onUpdateTimer, this);
|
||||
|
||||
// Initial load immediately
|
||||
updateTemperatures();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
if (timerID) {
|
||||
g_source_remove(timerID);
|
||||
}
|
||||
if (monitor) {
|
||||
delete monitor;
|
||||
}
|
||||
if (chart) {
|
||||
delete chart;
|
||||
}
|
||||
if (config) {
|
||||
config->save();
|
||||
delete config;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setupUI()
|
||||
{
|
||||
window = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window), "NVMe Temperature Monitor");
|
||||
|
||||
// Set window icon - in GTK4 we load pixbuf and use it for the window display
|
||||
GError *error = nullptr;
|
||||
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_resource("/org/kamma/nvme-monitor/nvme-monitor.svg", &error);
|
||||
if (pixbuf) {
|
||||
// GTK4 doesn't have gtk_window_set_icon, but we can store the pixbuf for use
|
||||
// The icon will be visible in the window title bar and taskbar through the name
|
||||
g_object_unref(pixbuf);
|
||||
} else if (error) {
|
||||
g_warning("Failed to load icon from resources: %s", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
// Set icon name for fallback/system icon theme
|
||||
gtk_window_set_icon_name(GTK_WINDOW(window), "nvme-monitor");
|
||||
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), 1200, 700);
|
||||
g_signal_connect(window, "close-request", G_CALLBACK(onDeleteWindow), this);
|
||||
|
||||
// Main box
|
||||
GtkWidget *mainBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_widget_set_margin_top(mainBox, 5);
|
||||
gtk_widget_set_margin_start(mainBox, 5);
|
||||
gtk_widget_set_margin_end(mainBox, 5);
|
||||
gtk_widget_set_margin_bottom(mainBox, 5);
|
||||
gtk_window_set_child(GTK_WINDOW(window), mainBox);
|
||||
|
||||
// Control panel
|
||||
GtkWidget *controlBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
|
||||
// Refresh rate label
|
||||
GtkWidget *refreshLabel = gtk_label_new("Refresh interval (s):");
|
||||
gtk_box_append(GTK_BOX(controlBox), refreshLabel);
|
||||
|
||||
// Refresh rate spinner
|
||||
GtkAdjustment *adjustment = gtk_adjustment_new(refreshRateSec, 1, 60, 1, 5, 0);
|
||||
refreshRateSpinBox = GTK_SPIN_BUTTON(gtk_spin_button_new(adjustment, 100, 0));
|
||||
g_signal_connect(refreshRateSpinBox, "value-changed", G_CALLBACK(onRefreshRateChanged), this);
|
||||
gtk_box_append(GTK_BOX(controlBox), GTK_WIDGET(refreshRateSpinBox));
|
||||
|
||||
// Clear button
|
||||
GtkWidget *clearButton = gtk_button_new_with_label("Clear Data");
|
||||
g_signal_connect(clearButton, "clicked", G_CALLBACK(onClearButtonClicked), this);
|
||||
gtk_box_append(GTK_BOX(controlBox), clearButton);
|
||||
|
||||
// Status label
|
||||
statusLabel = gtk_label_new("Initializing...");
|
||||
gtk_label_set_xalign(GTK_LABEL(statusLabel), 0);
|
||||
gtk_widget_set_hexpand(statusLabel, TRUE);
|
||||
gtk_box_append(GTK_BOX(controlBox), statusLabel);
|
||||
|
||||
// Quit button
|
||||
GtkWidget *quitButton = gtk_button_new_with_label("Quit");
|
||||
g_signal_connect(quitButton, "clicked", G_CALLBACK(onQuitButtonClicked), this);
|
||||
gtk_box_append(GTK_BOX(controlBox), quitButton);
|
||||
|
||||
gtk_box_append(GTK_BOX(mainBox), controlBox);
|
||||
|
||||
// Chart
|
||||
chart = new TemperatureChart();
|
||||
gtk_box_append(GTK_BOX(mainBox), chart->getWidget());
|
||||
gtk_widget_set_vexpand(chart->getWidget(), TRUE);
|
||||
|
||||
// Legend box
|
||||
legendBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_widget_set_margin_top(legendBox, 10);
|
||||
gtk_box_append(GTK_BOX(mainBox), legendBox);
|
||||
}
|
||||
|
||||
gboolean MainWindow::onDeleteWindow(GtkWidget *widget, gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
self->saveWindowState();
|
||||
delete self;
|
||||
|
||||
// Quit the main loop
|
||||
if (gMainLoop) {
|
||||
g_main_loop_quit(gMainLoop);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean MainWindow::onUpdateTimer(gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
self->updateTemperatures();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void MainWindow::onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
self->refreshRateSec = gtk_spin_button_get_value_as_int(spinButton);
|
||||
self->config->setPollingTime(self->refreshRateSec);
|
||||
|
||||
// Update timer
|
||||
if (self->timerID) {
|
||||
g_source_remove(self->timerID);
|
||||
}
|
||||
self->timerID = g_timeout_add(self->refreshRateSec * 1000, onUpdateTimer, self);
|
||||
}
|
||||
|
||||
void MainWindow::onClearButtonClicked(GtkButton *button, gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
self->chart->clear();
|
||||
}
|
||||
|
||||
void MainWindow::onQuitButtonClicked(GtkButton *button, gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
self->saveWindowState();
|
||||
|
||||
// Quit the main loop
|
||||
if (gMainLoop) {
|
||||
g_main_loop_quit(gMainLoop);
|
||||
}
|
||||
|
||||
delete self;
|
||||
}
|
||||
|
||||
void MainWindow::saveWindowState()
|
||||
{
|
||||
int width, height;
|
||||
gtk_window_get_default_size(GTK_WINDOW(window), &width, &height);
|
||||
|
||||
config->setWindowWidth(width > 0 ? width : 1200);
|
||||
config->setWindowHeight(height > 0 ? height : 700);
|
||||
config->setPollingTime(refreshRateSec);
|
||||
config->save();
|
||||
}
|
||||
|
||||
void MainWindow::updateTemperatures()
|
||||
{
|
||||
auto allTemps = monitor->getAllTemperatures();
|
||||
|
||||
// Get current real time in milliseconds since epoch
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
int64_t currentTime = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
int totalReadings = 0;
|
||||
|
||||
for (auto &deviceEntry : allTemps) {
|
||||
const std::string &device = deviceEntry.first;
|
||||
const auto &temps = deviceEntry.second;
|
||||
|
||||
for (const auto &tempEntry : temps) {
|
||||
const std::string &sensorName = tempEntry.first;
|
||||
double temperature = tempEntry.second;
|
||||
|
||||
chart->addTemperatureData(device, sensorName, temperature, currentTime);
|
||||
totalReadings++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status label
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm *timeinfo = std::localtime(&now);
|
||||
std::ostringstream oss;
|
||||
oss << "Updated: " << std::put_time(timeinfo, "%H:%M:%S")
|
||||
<< " | Devices: " << allTemps.size()
|
||||
<< " | Total readings: " << totalReadings;
|
||||
|
||||
gtk_label_set_text(GTK_LABEL(statusLabel), oss.str().c_str());
|
||||
|
||||
// Update legend
|
||||
updateLegend();
|
||||
}
|
||||
|
||||
void MainWindow::updateLegend()
|
||||
{
|
||||
// Clear existing legend items
|
||||
GtkWidget *child = gtk_widget_get_first_child(legendBox);
|
||||
while (child) {
|
||||
GtkWidget *next = gtk_widget_get_next_sibling(child);
|
||||
gtk_box_remove(GTK_BOX(legendBox), child);
|
||||
child = next;
|
||||
}
|
||||
|
||||
// Get device colors from chart
|
||||
auto deviceColors = chart->getDeviceColors();
|
||||
|
||||
// Add legend items
|
||||
for (const auto &pair : deviceColors) {
|
||||
const std::string &device = pair.first;
|
||||
const GdkRGBA &color = pair.second;
|
||||
|
||||
// Create container for legend item
|
||||
GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
// Create color box (drawing area with fixed color)
|
||||
GtkWidget *colorBox = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(colorBox, 20, 20);
|
||||
|
||||
// Store color in user data
|
||||
GdkRGBA *colorPtr = new GdkRGBA(color);
|
||||
g_object_set_data_full(G_OBJECT(colorBox), "color", colorPtr,
|
||||
[](gpointer data) { delete static_cast<GdkRGBA*>(data); });
|
||||
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(colorBox),
|
||||
[](GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) {
|
||||
GdkRGBA *color = static_cast<GdkRGBA*>(data);
|
||||
cairo_set_source_rgba(cr, color->red, color->green, color->blue, color->alpha);
|
||||
cairo_paint(cr);
|
||||
|
||||
// Get theme colors from GTK settings
|
||||
gboolean isDark = FALSE;
|
||||
GtkSettings *settings = gtk_settings_get_default();
|
||||
if (settings) {
|
||||
gchar *themeName = nullptr;
|
||||
g_object_get(settings, "gtk-theme-name", &themeName, nullptr);
|
||||
if (themeName) {
|
||||
std::string theme(themeName);
|
||||
isDark = (theme.find("dark") != std::string::npos ||
|
||||
theme.find("Dark") != std::string::npos);
|
||||
g_free(themeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Try GSetting if not found in GTK settings
|
||||
if (!isDark) {
|
||||
GSettingsSchemaSource *source = g_settings_schema_source_get_default();
|
||||
if (source) {
|
||||
GSettingsSchema *schema = g_settings_schema_source_lookup(source, "org.gnome.desktop.interface", FALSE);
|
||||
if (schema) {
|
||||
GSettings *gsettings = g_settings_new("org.gnome.desktop.interface");
|
||||
if (gsettings) {
|
||||
gchar *gtkTheme = g_settings_get_string(gsettings, "gtk-theme");
|
||||
if (gtkTheme) {
|
||||
std::string theme(gtkTheme);
|
||||
isDark = (theme.find("dark") != std::string::npos ||
|
||||
theme.find("Dark") != std::string::npos);
|
||||
g_free(gtkTheme);
|
||||
}
|
||||
g_object_unref(gsettings);
|
||||
}
|
||||
g_settings_schema_unref(schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); // Lighter gray for dark theme
|
||||
} else {
|
||||
cairo_set_source_rgb(cr, 0.3, 0.3, 0.3); // Darker gray for light theme
|
||||
}
|
||||
cairo_set_line_width(cr, 1);
|
||||
cairo_rectangle(cr, 0, 0, width, height);
|
||||
cairo_stroke(cr);
|
||||
}, colorPtr, nullptr);
|
||||
|
||||
gtk_box_append(GTK_BOX(itemBox), colorBox);
|
||||
|
||||
// Create label with device name
|
||||
GtkWidget *label = gtk_label_new(device.c_str());
|
||||
gtk_box_append(GTK_BOX(itemBox), label);
|
||||
|
||||
gtk_box_append(GTK_BOX(legendBox), itemBox);
|
||||
}
|
||||
|
||||
gtk_widget_set_visible(legendBox, TRUE);
|
||||
}
|
||||
|
||||
void MainWindow::show()
|
||||
{
|
||||
gtk_widget_set_visible(window, TRUE);
|
||||
}
|
||||
164
src/nvme_monitor.cpp
Normal file
164
src/nvme_monitor.cpp
Normal file
@ -0,0 +1,164 @@
|
||||
#include "nvme_monitor.h"
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <iostream>
|
||||
#include <cctype>
|
||||
#include <algorithm>
|
||||
|
||||
NvmeMonitor::NvmeMonitor()
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<std::string> NvmeMonitor::getAvailableDevices()
|
||||
{
|
||||
return scanDevices();
|
||||
}
|
||||
|
||||
double NvmeMonitor::readTemperatureFromFile(const std::string &filePath)
|
||||
{
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
if (std::getline(file, line)) {
|
||||
try {
|
||||
// Temperature is in millidegrees Celsius, convert to Celsius
|
||||
long tempMilliC = std::stol(line);
|
||||
return tempMilliC / 1000.0;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Failed to parse temperature from " << filePath << ": " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> NvmeMonitor::findHwmonDevices()
|
||||
{
|
||||
std::map<std::string, std::string> hwmonMap; // Maps device name to hwmon path
|
||||
|
||||
DIR* hwmonDir = opendir("/sys/class/hwmon");
|
||||
if (!hwmonDir) return hwmonMap;
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(hwmonDir)) != nullptr) {
|
||||
std::string hwmonName = entry->d_name;
|
||||
|
||||
// Look for hwmon* directories
|
||||
if (hwmonName.find("hwmon") == 0) {
|
||||
std::string namePath = "/sys/class/hwmon/" + hwmonName + "/name";
|
||||
std::ifstream nameFile(namePath);
|
||||
if (nameFile.is_open()) {
|
||||
std::string deviceName;
|
||||
if (std::getline(nameFile, deviceName)) {
|
||||
// Remove trailing whitespace
|
||||
deviceName.erase(deviceName.find_last_not_of(" \n\r\t") + 1);
|
||||
|
||||
if (deviceName == "nvme") {
|
||||
hwmonMap[hwmonName] = "/sys/class/hwmon/" + hwmonName;
|
||||
}
|
||||
}
|
||||
nameFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(hwmonDir);
|
||||
|
||||
return hwmonMap;
|
||||
}
|
||||
|
||||
std::vector<std::string> NvmeMonitor::scanDevices()
|
||||
{
|
||||
std::vector<std::string> devices;
|
||||
|
||||
DIR* devDir = opendir("/dev");
|
||||
if (!devDir) return devices;
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(devDir)) != nullptr) {
|
||||
std::string name = entry->d_name;
|
||||
|
||||
// Only include main device entries (nvme0, nvme1, etc.), not namespaces or fabrics
|
||||
if (name.find("nvme") == 0 && name.length() > 4) {
|
||||
// Check if the rest after "nvme" contains only digits
|
||||
bool onlyDigits = true;
|
||||
for (size_t i = 4; i < name.length(); i++) {
|
||||
if (!std::isdigit(name[i])) {
|
||||
onlyDigits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onlyDigits) {
|
||||
devices.push_back("/dev/" + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(devDir);
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
std::map<std::string, double> NvmeMonitor::readTemperatures(const std::string &device)
|
||||
{
|
||||
std::map<std::string, double> temperatures;
|
||||
|
||||
// Map hwmon devices to NVMe devices
|
||||
auto hwmonMap = findHwmonDevices();
|
||||
|
||||
if (hwmonMap.empty()) {
|
||||
std::cerr << "No NVMe hwmon devices found in /sys/class/hwmon" << std::endl;
|
||||
return temperatures;
|
||||
}
|
||||
|
||||
// For now, read from the first NVMe device found
|
||||
// In a more sophisticated implementation, we could map specific nvme devices to hwmon entries
|
||||
auto firstHwmon = hwmonMap.begin();
|
||||
std::string hwmonPath = firstHwmon->second;
|
||||
|
||||
// Read temperature sensors from hwmon
|
||||
// temp1_input is typically the main sensor
|
||||
double temp1 = readTemperatureFromFile(hwmonPath + "/temp1_input");
|
||||
if (temp1 > 0) {
|
||||
temperatures["Main"] = temp1;
|
||||
}
|
||||
|
||||
// Check for additional temperature sensors (temp2, temp3, etc.)
|
||||
for (int i = 2; i <= 10; i++) {
|
||||
std::string tempPath = hwmonPath + "/temp" + std::to_string(i) + "_input";
|
||||
struct stat buffer;
|
||||
if (stat(tempPath.c_str(), &buffer) == 0) {
|
||||
double temp = readTemperatureFromFile(tempPath);
|
||||
if (temp > 0) {
|
||||
std::string sensorName = "Sensor " + std::to_string(i);
|
||||
temperatures[sensorName] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (temperatures.empty()) {
|
||||
std::cerr << "Failed to read temperatures from device: " << device << std::endl;
|
||||
}
|
||||
|
||||
return temperatures;
|
||||
}
|
||||
|
||||
std::map<std::string, std::map<std::string, double>> NvmeMonitor::getAllTemperatures()
|
||||
{
|
||||
std::map<std::string, std::map<std::string, double>> allTemperatures;
|
||||
|
||||
std::vector<std::string> devices = scanDevices();
|
||||
for (const auto &device : devices) {
|
||||
auto temps = readTemperatures(device);
|
||||
if (!temps.empty()) {
|
||||
allTemperatures[device] = temps;
|
||||
}
|
||||
}
|
||||
|
||||
return allTemperatures;
|
||||
}
|
||||
469
src/temperature_chart.cpp
Normal file
469
src/temperature_chart.cpp
Normal file
@ -0,0 +1,469 @@
|
||||
#include "temperature_chart.h"
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
TemperatureChart::TemperatureChart(GtkWidget *parent)
|
||||
: drawingArea(nullptr), tooltipWindow(nullptr), tooltipLabel(nullptr),
|
||||
maxDataPoints(600), tickHandler(0),
|
||||
minTemp(MIN_TEMP), maxTemp(MAX_TEMP), minTime(0), maxTime(0),
|
||||
lastMouseX(-1), lastMouseY(-1)
|
||||
{
|
||||
drawingArea = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(drawingArea, 800, 400);
|
||||
gtk_widget_set_can_focus(drawingArea, TRUE);
|
||||
gtk_widget_set_focusable(drawingArea, TRUE);
|
||||
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(drawingArea),
|
||||
[](GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer userData) {
|
||||
TemperatureChart *self = static_cast<TemperatureChart*>(userData);
|
||||
self->drawChart(area, cr, width, height);
|
||||
}, this, nullptr);
|
||||
|
||||
// Setup event controller for mouse motion
|
||||
GtkEventController *motion = gtk_event_controller_motion_new();
|
||||
|
||||
g_signal_connect_data(motion, "motion",
|
||||
G_CALLBACK(+[](GtkEventControllerMotion *motion, double x, double y, gpointer userData) -> gboolean {
|
||||
TemperatureChart *self = static_cast<TemperatureChart*>(userData);
|
||||
self->lastMouseX = x;
|
||||
self->lastMouseY = y;
|
||||
|
||||
GtkWidget *drawArea = self->drawingArea;
|
||||
int width = gtk_widget_get_width(drawArea);
|
||||
int height = gtk_widget_get_height(drawArea);
|
||||
|
||||
NearestPoint nearest = self->findNearestDataPoint(x, y, width, height);
|
||||
|
||||
if (nearest.found && nearest.distance < 5.0) {
|
||||
self->showTooltip(x, y, nearest);
|
||||
} else {
|
||||
self->hideTooltip();
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}), this, nullptr, (GConnectFlags)0);
|
||||
|
||||
g_signal_connect(motion, "leave", G_CALLBACK(onLeave), this);
|
||||
gtk_widget_add_controller(drawingArea, motion);
|
||||
|
||||
setupColors();
|
||||
|
||||
// Setup tick callback for redrawing
|
||||
tickHandler = g_timeout_add(100, onTick, this);
|
||||
}
|
||||
|
||||
TemperatureChart::~TemperatureChart()
|
||||
{
|
||||
if (tickHandler) {
|
||||
g_source_remove(tickHandler);
|
||||
}
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
void TemperatureChart::setupColors()
|
||||
{
|
||||
// Define colors for different devices
|
||||
const GdkRGBA colors[] = {
|
||||
{1.0, 0.0, 0.0, 1.0}, // Red
|
||||
{0.0, 0.0, 1.0, 1.0}, // Blue
|
||||
{0.0, 0.5, 0.0, 1.0}, // Green
|
||||
{1.0, 0.647, 0.0, 1.0}, // Orange
|
||||
{0.5, 0.0, 0.5, 1.0}, // Purple
|
||||
{0.0, 0.5, 0.5, 1.0}, // Teal
|
||||
{1.0, 0.753, 0.796, 1.0}, // Pink
|
||||
{0.647, 0.165, 0.165, 1.0} // Brown
|
||||
};
|
||||
|
||||
int index = 0;
|
||||
for (auto &entry : seriesMap) {
|
||||
entry.second.color = colors[index % 8];
|
||||
index++;
|
||||
}
|
||||
|
||||
updateThemeColors();
|
||||
}
|
||||
|
||||
void TemperatureChart::updateThemeColors()
|
||||
{
|
||||
// Detect dark mode by checking the desktop theme
|
||||
gboolean isDark = FALSE;
|
||||
|
||||
// Try to get the theme from both GTK settings and Gnome settings
|
||||
GtkSettings *settings = gtk_settings_get_default();
|
||||
if (settings) {
|
||||
gchar *themeName = nullptr;
|
||||
g_object_get(settings, "gtk-theme-name", &themeName, nullptr);
|
||||
if (themeName) {
|
||||
std::string theme(themeName);
|
||||
// Check if theme name contains "dark"
|
||||
isDark = (theme.find("dark") != std::string::npos ||
|
||||
theme.find("Dark") != std::string::npos);
|
||||
g_free(themeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Also try GSetting if available
|
||||
if (!isDark) {
|
||||
GSettingsSchemaSource *source = g_settings_schema_source_get_default();
|
||||
if (source) {
|
||||
GSettingsSchema *schema = g_settings_schema_source_lookup(source, "org.gnome.desktop.interface", FALSE);
|
||||
if (schema) {
|
||||
GSettings *gsettings = g_settings_new("org.gnome.desktop.interface");
|
||||
if (gsettings) {
|
||||
gchar *gtkTheme = g_settings_get_string(gsettings, "gtk-theme");
|
||||
if (gtkTheme) {
|
||||
std::string theme(gtkTheme);
|
||||
isDark = (theme.find("dark") != std::string::npos ||
|
||||
theme.find("Dark") != std::string::npos);
|
||||
g_free(gtkTheme);
|
||||
}
|
||||
g_object_unref(gsettings);
|
||||
}
|
||||
g_settings_schema_unref(schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
// Dark theme colors
|
||||
bgColor = {0.1, 0.1, 0.1, 1.0}; // Dark background
|
||||
textColor = {0.9, 0.9, 0.9, 1.0}; // Light text
|
||||
gridColor = {0.25, 0.25, 0.25, 1.0}; // Dark gray grid
|
||||
axisColor = {0.9, 0.9, 0.9, 1.0}; // Light axis
|
||||
} else {
|
||||
// Light theme colors
|
||||
bgColor = {1.0, 1.0, 1.0, 1.0}; // White background
|
||||
textColor = {0.0, 0.0, 0.0, 1.0}; // Black text
|
||||
gridColor = {0.9, 0.9, 0.9, 1.0}; // Light gray grid
|
||||
axisColor = {0.0, 0.0, 0.0, 1.0}; // Black axis
|
||||
}
|
||||
}
|
||||
|
||||
GdkRGBA TemperatureChart::getColorForDevice(const std::string &device)
|
||||
{
|
||||
if (colorMap.find(device) == colorMap.end()) {
|
||||
const GdkRGBA colors[] = {
|
||||
{1.0, 0.0, 0.0, 1.0},
|
||||
{0.0, 0.0, 1.0, 1.0},
|
||||
{0.0, 0.5, 0.0, 1.0},
|
||||
{1.0, 0.647, 0.0, 1.0},
|
||||
{0.5, 0.0, 0.5, 1.0},
|
||||
{0.0, 0.5, 0.5, 1.0},
|
||||
{1.0, 0.753, 0.796, 1.0},
|
||||
{0.647, 0.165, 0.165, 1.0}
|
||||
};
|
||||
|
||||
int colorIndex = colorMap.size() % 8;
|
||||
colorMap[device] = colors[colorIndex];
|
||||
}
|
||||
|
||||
return colorMap[device];
|
||||
}
|
||||
|
||||
void TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor,
|
||||
double temperature, int64_t timestamp)
|
||||
{
|
||||
std::string seriesKey = device + " - " + sensor;
|
||||
|
||||
// Create series if it doesn't exist
|
||||
if (seriesMap.find(seriesKey) == seriesMap.end()) {
|
||||
SeriesData series;
|
||||
series.color = getColorForDevice(device);
|
||||
series.name = seriesKey;
|
||||
seriesMap[seriesKey] = series;
|
||||
}
|
||||
|
||||
// Add data point
|
||||
DataPoint point;
|
||||
point.temperature = temperature;
|
||||
point.timestamp = timestamp;
|
||||
|
||||
seriesMap[seriesKey].points.push_back(point);
|
||||
|
||||
// Keep only last 10 minutes of data
|
||||
int64_t cutoffTime = timestamp - MAX_TIME_MS;
|
||||
for (auto &entry : seriesMap) {
|
||||
auto &points = entry.second.points;
|
||||
auto it = points.begin();
|
||||
while (it != points.end() && it->timestamp < cutoffTime) {
|
||||
it = points.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Update temperature range
|
||||
minTemp = MIN_TEMP;
|
||||
maxTemp = MAX_TEMP;
|
||||
|
||||
// Update time range - keep 10 minute window
|
||||
maxTime = timestamp;
|
||||
minTime = timestamp - MAX_TIME_MS;
|
||||
|
||||
// Trigger redraw
|
||||
gtk_widget_queue_draw(drawingArea);
|
||||
}
|
||||
|
||||
void TemperatureChart::clear()
|
||||
{
|
||||
seriesMap.clear();
|
||||
colorMap.clear();
|
||||
minTemp = MIN_TEMP;
|
||||
maxTemp = MAX_TEMP;
|
||||
minTime = 0;
|
||||
maxTime = 0;
|
||||
gtk_widget_queue_draw(drawingArea);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getDeviceColors() const
|
||||
{
|
||||
std::vector<std::pair<std::string, GdkRGBA>> result;
|
||||
for (const auto &entry : colorMap) {
|
||||
result.push_back({entry.first, entry.second});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
gboolean TemperatureChart::onTick(gpointer userData)
|
||||
{
|
||||
TemperatureChart *self = static_cast<TemperatureChart*>(userData);
|
||||
gtk_widget_queue_draw(self->drawingArea);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height)
|
||||
{
|
||||
// Update theme colors before drawing
|
||||
updateThemeColors();
|
||||
|
||||
// Background with theme color
|
||||
cairo_set_source_rgb(cr, bgColor.red, bgColor.green, bgColor.blue);
|
||||
cairo_paint(cr);
|
||||
|
||||
if (seriesMap.empty() || maxTime == minTime) {
|
||||
// Draw placeholder
|
||||
cairo_set_source_rgb(cr, textColor.red * 0.6, textColor.green * 0.6, textColor.blue * 0.6);
|
||||
cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
|
||||
cairo_set_font_size(cr, 14);
|
||||
cairo_move_to(cr, width / 2 - 100, height / 2);
|
||||
cairo_show_text(cr, "Waiting for data...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Margins
|
||||
double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40;
|
||||
double plotWidth = width - marginLeft - marginRight;
|
||||
double plotHeight = height - marginTop - marginBottom;
|
||||
|
||||
// Draw grid and axes
|
||||
cairo_set_source_rgb(cr, gridColor.red, gridColor.green, gridColor.blue);
|
||||
cairo_set_line_width(cr, 0.5);
|
||||
|
||||
// Y axis grid lines (temperature) - every 10 degrees
|
||||
for (double temp = minTemp; temp <= maxTemp; temp += 10.0) {
|
||||
double y = marginTop + plotHeight * (1.0 - (temp - minTemp) / (maxTemp - minTemp));
|
||||
cairo_move_to(cr, marginLeft, y);
|
||||
cairo_line_to(cr, marginLeft + plotWidth, y);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
// X axis grid lines (time) - 5 lines for 10-minute window
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
double x = marginLeft + (plotWidth * i / 5.0);
|
||||
cairo_move_to(cr, x, marginTop);
|
||||
cairo_line_to(cr, x, marginTop + plotHeight);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
// Draw axes
|
||||
cairo_set_source_rgb(cr, axisColor.red, axisColor.green, axisColor.blue);
|
||||
cairo_set_line_width(cr, 2);
|
||||
cairo_move_to(cr, marginLeft, marginTop);
|
||||
cairo_line_to(cr, marginLeft, marginTop + plotHeight);
|
||||
cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight);
|
||||
cairo_stroke(cr);
|
||||
|
||||
// Draw axis labels
|
||||
cairo_set_source_rgb(cr, textColor.red, textColor.green, textColor.blue);
|
||||
cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
|
||||
cairo_set_font_size(cr, 10);
|
||||
|
||||
// Temperature labels every 10 degrees on Y axis
|
||||
for (double temp = minTemp; temp <= maxTemp; temp += 10.0) {
|
||||
double y = marginTop + plotHeight * (1.0 - (temp - minTemp) / (maxTemp - minTemp));
|
||||
|
||||
char tempStr[16];
|
||||
snprintf(tempStr, sizeof(tempStr), "%.0f°C", temp);
|
||||
|
||||
// Measure text width for proper right alignment
|
||||
cairo_text_extents_t extents;
|
||||
cairo_text_extents(cr, tempStr, &extents);
|
||||
|
||||
// Position text right-aligned, 8 pixels before the plot area
|
||||
double textX = marginLeft - 8 - extents.width;
|
||||
cairo_move_to(cr, textX, y + 4);
|
||||
cairo_show_text(cr, tempStr);
|
||||
}
|
||||
|
||||
// Time labels on X axis (5 points for 10-minute window)
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
double x = marginLeft + (plotWidth * i / 5.0);
|
||||
|
||||
// Calculate time offset in seconds
|
||||
int secondsOffset = (5 - i) * 120; // 10 minutes / 5 = 2 minutes between points
|
||||
int minutes = secondsOffset / 60;
|
||||
|
||||
char timeStr[16];
|
||||
snprintf(timeStr, sizeof(timeStr), "-%d m", minutes);
|
||||
|
||||
cairo_move_to(cr, x - 20, marginTop + plotHeight + 20);
|
||||
cairo_show_text(cr, timeStr);
|
||||
}
|
||||
|
||||
// Draw data series (right to left, with 10-minute window)
|
||||
for (auto &seriesEntry : seriesMap) {
|
||||
SeriesData &series = seriesEntry.second;
|
||||
|
||||
if (series.points.empty()) continue;
|
||||
|
||||
// Set series color
|
||||
cairo_set_source_rgba(cr, series.color.red, series.color.green,
|
||||
series.color.blue, series.color.alpha);
|
||||
cairo_set_line_width(cr, 2);
|
||||
|
||||
bool firstPoint = true;
|
||||
for (const DataPoint &point : series.points) {
|
||||
// Reverse X axis - newer data on the right, older on the left
|
||||
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS);
|
||||
double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp));
|
||||
|
||||
if (firstPoint) {
|
||||
cairo_move_to(cr, x, y);
|
||||
firstPoint = false;
|
||||
} else {
|
||||
cairo_line_to(cr, x, y);
|
||||
}
|
||||
}
|
||||
cairo_stroke(cr);
|
||||
|
||||
// Draw dots at data points
|
||||
cairo_set_source_rgba(cr, series.color.red, series.color.green,
|
||||
series.color.blue, series.color.alpha);
|
||||
for (const DataPoint &point : series.points) {
|
||||
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS);
|
||||
double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp));
|
||||
|
||||
cairo_arc(cr, x, y, 3, 0, 2 * M_PI);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw title
|
||||
cairo_set_source_rgb(cr, textColor.red, textColor.green, textColor.blue);
|
||||
cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
|
||||
cairo_set_font_size(cr, 16);
|
||||
cairo_move_to(cr, width / 2 - 150, 25);
|
||||
cairo_show_text(cr, "NVMe Disk Temperatures (Real-time)");
|
||||
}
|
||||
|
||||
TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mouseX, double mouseY, int width, int height)
|
||||
{
|
||||
NearestPoint result;
|
||||
result.found = false;
|
||||
result.distance = std::numeric_limits<double>::max();
|
||||
|
||||
if (seriesMap.empty() || maxTime == minTime) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Same margins as in drawChart
|
||||
double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40;
|
||||
double plotWidth = width - marginLeft - marginRight;
|
||||
double plotHeight = height - marginTop - marginBottom;
|
||||
|
||||
// Check if mouse is in plot area
|
||||
if (mouseX < marginLeft || mouseX > marginLeft + plotWidth ||
|
||||
mouseY < marginTop || mouseY > marginTop + plotHeight) {
|
||||
return result;
|
||||
}
|
||||
for (auto &seriesEntry : seriesMap) {
|
||||
SeriesData &series = seriesEntry.second;
|
||||
|
||||
for (const DataPoint &point : series.points) {
|
||||
double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS);
|
||||
double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp));
|
||||
|
||||
double distance = std::hypot(mouseX - x, mouseY - y);
|
||||
|
||||
if (distance < result.distance) {
|
||||
result.distance = distance;
|
||||
result.found = true;
|
||||
result.temperature = point.temperature;
|
||||
result.timestamp = point.timestamp;
|
||||
result.seriesName = seriesEntry.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point)
|
||||
{
|
||||
if (!tooltipWindow) {
|
||||
tooltipWindow = gtk_popover_new();
|
||||
tooltipLabel = gtk_label_new("");
|
||||
gtk_widget_set_margin_start(tooltipLabel, 8);
|
||||
gtk_widget_set_margin_end(tooltipLabel, 8);
|
||||
gtk_widget_set_margin_top(tooltipLabel, 5);
|
||||
gtk_widget_set_margin_bottom(tooltipLabel, 5);
|
||||
gtk_popover_set_child(GTK_POPOVER(tooltipWindow), tooltipLabel);
|
||||
gtk_popover_set_position(GTK_POPOVER(tooltipWindow), GTK_POS_TOP);
|
||||
gtk_widget_set_parent(tooltipWindow, drawingArea);
|
||||
}
|
||||
|
||||
// Format the tooltip text with real current time
|
||||
char tooltipText[256];
|
||||
|
||||
// Convert timestamp from milliseconds since epoch to time structure
|
||||
time_t timeSeconds = point.timestamp / 1000;
|
||||
struct tm *timeinfo = localtime(&timeSeconds);
|
||||
|
||||
char timeStr[32];
|
||||
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo);
|
||||
|
||||
snprintf(tooltipText, sizeof(tooltipText),
|
||||
"%s\n%.1f°C\n%s",
|
||||
point.seriesName.c_str(),
|
||||
point.temperature,
|
||||
timeStr);
|
||||
|
||||
gtk_label_set_text(GTK_LABEL(tooltipLabel), tooltipText);
|
||||
|
||||
// Position the tooltip
|
||||
GdkRectangle rect;
|
||||
rect.x = (int)x;
|
||||
rect.y = (int)y;
|
||||
rect.width = 1;
|
||||
rect.height = 1;
|
||||
gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect);
|
||||
|
||||
gtk_widget_set_visible(tooltipWindow, TRUE);
|
||||
}
|
||||
|
||||
void TemperatureChart::hideTooltip()
|
||||
{
|
||||
if (tooltipWindow) {
|
||||
gtk_widget_unparent(tooltipWindow);
|
||||
tooltipWindow = nullptr;
|
||||
tooltipLabel = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TemperatureChart::onLeave(GtkEventControllerMotion *motion, gpointer userData)
|
||||
{
|
||||
TemperatureChart *self = static_cast<TemperatureChart*>(userData);
|
||||
self->hideTooltip();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user