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