added cpu, line graph, some fixes
This commit is contained in:
parent
44f968d9db
commit
1519a1bee4
@ -31,7 +31,7 @@ add_custom_command(
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/mainwindow.cpp
|
||||
src/nvme_monitor.cpp
|
||||
src/temp_monitor.cpp
|
||||
src/temperature_chart.cpp
|
||||
src/config_manager.cpp
|
||||
${CMAKE_BINARY_DIR}/resources.c
|
||||
@ -39,7 +39,7 @@ set(SOURCES
|
||||
|
||||
set(HEADERS
|
||||
include/mainwindow.h
|
||||
include/nvme_monitor.h
|
||||
include/temp_monitor.h
|
||||
include/temperature_chart.h
|
||||
include/config_manager.h
|
||||
)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include "nvme_monitor.h"
|
||||
#include "temp_monitor.h"
|
||||
#include "temperature_chart.h"
|
||||
#include "config_manager.h"
|
||||
|
||||
@ -24,12 +24,13 @@ private:
|
||||
static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData);
|
||||
static void onClearButtonClicked(GtkButton *button, gpointer userData);
|
||||
static void onQuitButtonClicked(GtkButton *button, gpointer userData);
|
||||
static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData);
|
||||
|
||||
GtkWidget *window;
|
||||
GtkWidget *statusLabel;
|
||||
GtkSpinButton *refreshRateSpinBox;
|
||||
TemperatureChart *chart;
|
||||
NvmeMonitor *monitor;
|
||||
TempMonitor *monitor;
|
||||
ConfigManager *config;
|
||||
guint timerID;
|
||||
int refreshRateSec;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#ifndef NVME_MONITOR_H
|
||||
#define NVME_MONITOR_H
|
||||
#ifndef TEMP_MONITOR_H
|
||||
#define TEMP_MONITOR_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
@ -12,11 +12,11 @@ struct TemperatureData {
|
||||
long timestamp;
|
||||
};
|
||||
|
||||
class NvmeMonitor {
|
||||
class TempMonitor {
|
||||
public:
|
||||
explicit NvmeMonitor();
|
||||
explicit TempMonitor();
|
||||
|
||||
// Get list of available NVMe devices
|
||||
// Get list of available devices
|
||||
std::vector<std::string> getAvailableDevices();
|
||||
|
||||
// Read temperatures from a specific device
|
||||
@ -26,9 +26,7 @@ public:
|
||||
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
|
||||
#endif // TEMP_MONITOR_H
|
||||
@ -26,18 +26,19 @@ public:
|
||||
|
||||
GtkWidget* getWidget() const { return drawingArea; }
|
||||
|
||||
void addTemperatureData(const std::string &device, const std::string &sensor,
|
||||
bool 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;
|
||||
// Get list of series with their colors
|
||||
std::vector<std::pair<std::string, GdkRGBA>> getSeriesColors() const;
|
||||
void setSeriesColor(const std::string &seriesName, const GdkRGBA &color);
|
||||
void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height);
|
||||
|
||||
private:
|
||||
void setupColors();
|
||||
GdkRGBA getColorForDevice(const std::string &device);
|
||||
GdkRGBA getColorForSeries(const std::string &seriesKey);
|
||||
void redraw();
|
||||
void updateThemeColors();
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ MainWindow::MainWindow()
|
||||
: window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr),
|
||||
chart(nullptr), monitor(nullptr), config(nullptr), timerID(0), refreshRateSec(3)
|
||||
{
|
||||
monitor = new NvmeMonitor();
|
||||
monitor = new TempMonitor();
|
||||
config = new ConfigManager();
|
||||
config->load();
|
||||
|
||||
@ -192,6 +192,7 @@ void MainWindow::updateTemperatures()
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
int64_t currentTime = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
int totalReadings = 0;
|
||||
bool needsLegendUpdate = false;
|
||||
|
||||
for (auto &deviceEntry : allTemps) {
|
||||
const std::string &device = deviceEntry.first;
|
||||
@ -201,7 +202,9 @@ void MainWindow::updateTemperatures()
|
||||
const std::string &sensorName = tempEntry.first;
|
||||
double temperature = tempEntry.second;
|
||||
|
||||
chart->addTemperatureData(device, sensorName, temperature, currentTime);
|
||||
if (chart->addTemperatureData(device, sensorName, temperature, currentTime)) {
|
||||
needsLegendUpdate = true;
|
||||
}
|
||||
totalReadings++;
|
||||
}
|
||||
}
|
||||
@ -216,8 +219,10 @@ void MainWindow::updateTemperatures()
|
||||
|
||||
gtk_label_set_text(GTK_LABEL(statusLabel), oss.str().c_str());
|
||||
|
||||
// Update legend
|
||||
updateLegend();
|
||||
// Update legend ONLY if new sensors were found or legend is empty
|
||||
if (needsLegendUpdate || gtk_widget_get_first_child(legendBox) == nullptr) {
|
||||
updateLegend();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateLegend()
|
||||
@ -230,82 +235,32 @@ void MainWindow::updateLegend()
|
||||
child = next;
|
||||
}
|
||||
|
||||
// Get device colors from chart
|
||||
auto deviceColors = chart->getDeviceColors();
|
||||
// Get series colors from chart
|
||||
auto seriesColors = chart->getSeriesColors();
|
||||
|
||||
// Add legend items
|
||||
for (const auto &pair : deviceColors) {
|
||||
const std::string &device = pair.first;
|
||||
for (const auto &pair : seriesColors) {
|
||||
const std::string &seriesName = 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);
|
||||
// Create color dialog and button (modern GTK4 way)
|
||||
GtkColorDialog *dialog = gtk_color_dialog_new();
|
||||
GtkWidget *colorButton = gtk_color_dialog_button_new(dialog);
|
||||
gtk_color_dialog_button_set_rgba(GTK_COLOR_DIALOG_BUTTON(colorButton), &color);
|
||||
gtk_widget_set_size_request(colorButton, 24, 24);
|
||||
|
||||
// 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); });
|
||||
// Store series name in user data to know which series to update
|
||||
g_object_set_data_full(G_OBJECT(colorButton), "series-name", g_strdup(seriesName.c_str()), g_free);
|
||||
|
||||
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);
|
||||
g_signal_connect(colorButton, "notify::rgba", G_CALLBACK(onColorSet), this);
|
||||
|
||||
gtk_box_append(GTK_BOX(itemBox), colorBox);
|
||||
gtk_box_append(GTK_BOX(itemBox), colorButton);
|
||||
|
||||
// Create label with device name
|
||||
GtkWidget *label = gtk_label_new(device.c_str());
|
||||
// Create label with series name
|
||||
GtkWidget *label = gtk_label_new(seriesName.c_str());
|
||||
gtk_box_append(GTK_BOX(itemBox), label);
|
||||
|
||||
gtk_box_append(GTK_BOX(legendBox), itemBox);
|
||||
@ -318,3 +273,16 @@ void MainWindow::show()
|
||||
{
|
||||
gtk_widget_set_visible(window, TRUE);
|
||||
}
|
||||
|
||||
void MainWindow::onColorSet(GObject *object, GParamSpec *pspec, gpointer userData)
|
||||
{
|
||||
MainWindow *self = static_cast<MainWindow*>(userData);
|
||||
const char *seriesName = static_cast<const char*>(g_object_get_data(object, "series-name"));
|
||||
|
||||
if (seriesName) {
|
||||
const GdkRGBA *color = gtk_color_dialog_button_get_rgba(GTK_COLOR_DIALOG_BUTTON(object));
|
||||
if (color) {
|
||||
self->chart->setSeriesColor(seriesName, *color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
124
src/temp_monitor.cpp
Normal file
124
src/temp_monitor.cpp
Normal file
@ -0,0 +1,124 @@
|
||||
#include "temp_monitor.h"
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
TempMonitor::TempMonitor()
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<std::string> TempMonitor::getAvailableDevices()
|
||||
{
|
||||
std::vector<std::string> devices;
|
||||
auto all = getAllTemperatures();
|
||||
for (auto const& [name, sensors] : all) {
|
||||
devices.push_back(name);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
double TempMonitor::readTemperatureFromFile(const std::string &filePath)
|
||||
{
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
return -1000.0;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
if (std::getline(file, line)) {
|
||||
try {
|
||||
long tempMilliC = std::stol(line);
|
||||
return tempMilliC / 1000.0;
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
return -1000.0;
|
||||
}
|
||||
|
||||
std::map<std::string, double> TempMonitor::readTemperatures(const std::string &device)
|
||||
{
|
||||
auto all = getAllTemperatures();
|
||||
if (all.count(device)) {
|
||||
return all.at(device);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::map<std::string, std::map<std::string, double>> TempMonitor::getAllTemperatures()
|
||||
{
|
||||
std::map<std::string, std::map<std::string, double>> allTemperatures;
|
||||
|
||||
DIR* hwmonDir = opendir("/sys/class/hwmon");
|
||||
if (!hwmonDir) return allTemperatures;
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(hwmonDir)) != nullptr) {
|
||||
std::string hwmonName = entry->d_name;
|
||||
if (hwmonName.find("hwmon") != 0) continue;
|
||||
|
||||
std::string path = "/sys/class/hwmon/" + hwmonName;
|
||||
|
||||
// Read "name" from sysfs as requested
|
||||
std::string nameFromSysfs = "Unknown";
|
||||
std::ifstream nameFile(path + "/name");
|
||||
if (nameFile.is_open()) {
|
||||
std::getline(nameFile, nameFromSysfs);
|
||||
nameFromSysfs.erase(nameFromSysfs.find_last_not_of(" \n\r\t") + 1);
|
||||
nameFile.close();
|
||||
}
|
||||
|
||||
// Filter: Only interested in nvme and coretemp (CPU)
|
||||
if (nameFromSysfs != "nvme" && nameFromSysfs != "coretemp") continue;
|
||||
|
||||
std::map<std::string, double> sensors;
|
||||
DIR* dDir = opendir(path.c_str());
|
||||
if (dDir) {
|
||||
struct dirent* sEntry;
|
||||
while ((sEntry = readdir(dDir)) != nullptr) {
|
||||
std::string fname = sEntry->d_name;
|
||||
// Only temperature sensors (temp*_input)
|
||||
if (fname.find("temp") == 0 && fname.find("_input") != std::string::npos) {
|
||||
std::string id = fname.substr(4, fname.find("_input") - 4);
|
||||
|
||||
double temp = readTemperatureFromFile(path + "/" + fname);
|
||||
if (temp > -200.0) {
|
||||
// Use "label" from sysfs for sensor name if available
|
||||
std::string labelFromSysfs = "";
|
||||
std::ifstream labelFile(path + "/temp" + id + "_label");
|
||||
if (labelFile.is_open()) {
|
||||
std::getline(labelFile, labelFromSysfs);
|
||||
labelFromSysfs.erase(labelFromSysfs.find_last_not_of(" \n\r\t") + 1);
|
||||
labelFile.close();
|
||||
}
|
||||
|
||||
// For CPU (coretemp), filter only Package id 0
|
||||
if (nameFromSysfs == "coretemp") {
|
||||
if (labelFromSysfs != "Package id 0") continue;
|
||||
}
|
||||
|
||||
// For NVMe, filter out Composite sensor
|
||||
if (nameFromSysfs == "nvme") {
|
||||
if (labelFromSysfs == "Composite") continue;
|
||||
}
|
||||
|
||||
std::string finalSensorName = labelFromSysfs.empty() ? "temp" + id : labelFromSysfs;
|
||||
sensors[finalSensorName] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dDir);
|
||||
}
|
||||
|
||||
if (!sensors.empty()) {
|
||||
// Label device with its sysfs name and instance ID
|
||||
allTemperatures[nameFromSysfs + " (" + hwmonName + ")"] = sensors;
|
||||
}
|
||||
}
|
||||
closedir(hwmonDir);
|
||||
|
||||
return allTemperatures;
|
||||
}
|
||||
@ -60,8 +60,14 @@ TemperatureChart::~TemperatureChart()
|
||||
{
|
||||
if (tickHandler) {
|
||||
g_source_remove(tickHandler);
|
||||
tickHandler = 0;
|
||||
}
|
||||
|
||||
if (tooltipWindow) {
|
||||
gtk_widget_unparent(tooltipWindow);
|
||||
tooltipWindow = nullptr;
|
||||
tooltipLabel = nullptr;
|
||||
}
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
void TemperatureChart::setupColors()
|
||||
@ -143,9 +149,9 @@ void TemperatureChart::updateThemeColors()
|
||||
}
|
||||
}
|
||||
|
||||
GdkRGBA TemperatureChart::getColorForDevice(const std::string &device)
|
||||
GdkRGBA TemperatureChart::getColorForSeries(const std::string &seriesKey)
|
||||
{
|
||||
if (colorMap.find(device) == colorMap.end()) {
|
||||
if (colorMap.find(seriesKey) == colorMap.end()) {
|
||||
const GdkRGBA colors[] = {
|
||||
{1.0, 0.0, 0.0, 1.0},
|
||||
{0.0, 0.0, 1.0, 1.0},
|
||||
@ -158,23 +164,25 @@ GdkRGBA TemperatureChart::getColorForDevice(const std::string &device)
|
||||
};
|
||||
|
||||
int colorIndex = colorMap.size() % 8;
|
||||
colorMap[device] = colors[colorIndex];
|
||||
colorMap[seriesKey] = colors[colorIndex];
|
||||
}
|
||||
|
||||
return colorMap[device];
|
||||
return colorMap[seriesKey];
|
||||
}
|
||||
|
||||
void TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor,
|
||||
bool TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor,
|
||||
double temperature, int64_t timestamp)
|
||||
{
|
||||
std::string seriesKey = device + " - " + sensor;
|
||||
bool isNew = false;
|
||||
|
||||
// Create series if it doesn't exist
|
||||
if (seriesMap.find(seriesKey) == seriesMap.end()) {
|
||||
SeriesData series;
|
||||
series.color = getColorForDevice(device);
|
||||
series.color = getColorForSeries(seriesKey);
|
||||
series.name = seriesKey;
|
||||
seriesMap[seriesKey] = series;
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
// Add data point
|
||||
@ -204,6 +212,8 @@ void TemperatureChart::addTemperatureData(const std::string &device, const std::
|
||||
|
||||
// Trigger redraw
|
||||
gtk_widget_queue_draw(drawingArea);
|
||||
|
||||
return isNew;
|
||||
}
|
||||
|
||||
void TemperatureChart::clear()
|
||||
@ -217,7 +227,7 @@ void TemperatureChart::clear()
|
||||
gtk_widget_queue_draw(drawingArea);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getDeviceColors() const
|
||||
std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getSeriesColors() const
|
||||
{
|
||||
std::vector<std::pair<std::string, GdkRGBA>> result;
|
||||
for (const auto &entry : colorMap) {
|
||||
@ -226,6 +236,19 @@ std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getDeviceColors()
|
||||
return result;
|
||||
}
|
||||
|
||||
void TemperatureChart::setSeriesColor(const std::string &seriesName, const GdkRGBA &color)
|
||||
{
|
||||
// Update color map
|
||||
colorMap[seriesName] = color;
|
||||
|
||||
// Update series if it exists
|
||||
if (seriesMap.find(seriesName) != seriesMap.end()) {
|
||||
seriesMap[seriesName].color = color;
|
||||
}
|
||||
|
||||
gtk_widget_queue_draw(drawingArea);
|
||||
}
|
||||
|
||||
gboolean TemperatureChart::onTick(gpointer userData)
|
||||
{
|
||||
TemperatureChart *self = static_cast<TemperatureChart*>(userData);
|
||||
@ -253,7 +276,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
|
||||
}
|
||||
|
||||
// Margins
|
||||
double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40;
|
||||
double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40;
|
||||
double plotWidth = width - marginLeft - marginRight;
|
||||
double plotHeight = height - marginTop - marginBottom;
|
||||
|
||||
@ -280,9 +303,9 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
|
||||
// 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_move_to(cr, marginLeft + plotWidth, marginTop);
|
||||
cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight);
|
||||
cairo_line_to(cr, marginLeft, marginTop + plotHeight);
|
||||
cairo_stroke(cr);
|
||||
|
||||
// Draw axis labels
|
||||
@ -297,12 +320,12 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
|
||||
char tempStr[16];
|
||||
snprintf(tempStr, sizeof(tempStr), "%.0f°C", temp);
|
||||
|
||||
// Measure text width for proper right alignment
|
||||
// Measure text width for proper 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;
|
||||
// Position text 8 pixels after the plot area on the right
|
||||
double textX = marginLeft + plotWidth + 8;
|
||||
cairo_move_to(cr, textX, y + 4);
|
||||
cairo_show_text(cr, tempStr);
|
||||
}
|
||||
@ -331,7 +354,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
|
||||
// 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);
|
||||
cairo_set_line_width(cr, 2.0);
|
||||
|
||||
bool firstPoint = true;
|
||||
for (const DataPoint &point : series.points) {
|
||||
@ -347,17 +370,6 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -379,7 +391,7 @@ TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mou
|
||||
}
|
||||
|
||||
// Same margins as in drawChart
|
||||
double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40;
|
||||
double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40;
|
||||
double plotWidth = width - marginLeft - marginRight;
|
||||
double plotHeight = height - marginTop - marginBottom;
|
||||
|
||||
@ -421,6 +433,8 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point
|
||||
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_popover_set_has_arrow(GTK_POPOVER(tooltipWindow), TRUE);
|
||||
gtk_popover_set_autohide(GTK_POPOVER(tooltipWindow), FALSE);
|
||||
gtk_widget_set_parent(tooltipWindow, drawingArea);
|
||||
}
|
||||
|
||||
@ -431,8 +445,10 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point
|
||||
time_t timeSeconds = point.timestamp / 1000;
|
||||
struct tm *timeinfo = localtime(&timeSeconds);
|
||||
|
||||
char timeStr[32];
|
||||
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo);
|
||||
char timeStr[32] = "??:??:??";
|
||||
if (timeinfo) {
|
||||
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo);
|
||||
}
|
||||
|
||||
snprintf(tooltipText, sizeof(tooltipText),
|
||||
"%s\n%.1f°C\n%s",
|
||||
@ -450,15 +466,15 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point
|
||||
rect.height = 1;
|
||||
gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect);
|
||||
|
||||
gtk_widget_set_visible(tooltipWindow, TRUE);
|
||||
if (!gtk_widget_get_visible(tooltipWindow)) {
|
||||
gtk_widget_set_visible(tooltipWindow, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
void TemperatureChart::hideTooltip()
|
||||
{
|
||||
if (tooltipWindow) {
|
||||
gtk_widget_unparent(tooltipWindow);
|
||||
tooltipWindow = nullptr;
|
||||
tooltipLabel = nullptr;
|
||||
if (tooltipWindow && gtk_widget_get_visible(tooltipWindow)) {
|
||||
gtk_widget_set_visible(tooltipWindow, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user