From de8081ebc5dce76aac8e00a41a0eb5416ed51038 Mon Sep 17 00:00:00 2001 From: damachine Date: Fri, 13 Feb 2026 21:49:58 +0100 Subject: [PATCH 1/8] VERSION: prepare next release - disable udev rules --- VERSION | 2 +- etc/udev/rules.d/99-coolerdash.rules | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 530cdd9..21bb5e1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.4 +2.2.5 diff --git a/etc/udev/rules.d/99-coolerdash.rules b/etc/udev/rules.d/99-coolerdash.rules index 2043616..b1d32f0 100644 --- a/etc/udev/rules.d/99-coolerdash.rules +++ b/etc/udev/rules.d/99-coolerdash.rules @@ -4,10 +4,10 @@ # NZXT Vendor ID: 1e71 # Disable autosuspend and enable persist for all NZXT devices (Kraken, etc.) # Disable USB autosuspend for NZXT devices to prevent "bucket switch" errors -ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/control}="on" -ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/autosuspend_delay_ms}="-1" -ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/persist}="1" -ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/wakeup}="disabled" -ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{avoid_reset_quirk}="1" +#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/control}="on" +#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/autosuspend_delay_ms}="-1" +#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/persist}="1" +#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/wakeup}="disabled" +#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{avoid_reset_quirk}="1" # Placeholder for additional rules if needed in the future From d976eaf0cf8dde9759790985fcdab99371ccf1af Mon Sep 17 00:00:00 2001 From: damachine Date: Fri, 13 Feb 2026 22:22:52 +0100 Subject: [PATCH 2/8] fix openapi type field name --- src/srv/cc_conf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/srv/cc_conf.c b/src/srv/cc_conf.c index a6211c1..9a451f1 100644 --- a/src/srv/cc_conf.c +++ b/src/srv/cc_conf.c @@ -79,7 +79,7 @@ const char *extract_device_type_from_json(const json_t *dev) if (!dev) return NULL; - const json_t *type_val = json_object_get(dev, "type"); + const json_t *type_val = json_object_get(dev, "d_type"); if (!type_val || !json_is_string(type_val)) return NULL; From 47c37094f65c89ceec4be2990a748fe6e51e5828 Mon Sep 17 00:00:00 2001 From: damachine Date: Fri, 13 Feb 2026 22:38:11 +0100 Subject: [PATCH 3/8] fix read wrong device in ui tab, revert openapi commit --- .SRCINFO | 2 +- src/srv/cc_conf.c | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.SRCINFO b/.SRCINFO index 215beed..01a041e 100644 --- a/.SRCINFO +++ b/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = coolerdash pkgdesc = Monitor telemetry data on an AIO liquid cooler with an integrated LCD display - pkgver = 2.2.4 + pkgver = 2.2.5 pkgrel = 1 url = https://github.com/damachine/coolerdash install = coolerdash.install diff --git a/src/srv/cc_conf.c b/src/srv/cc_conf.c index 9a451f1..5e6cff8 100644 --- a/src/srv/cc_conf.c +++ b/src/srv/cc_conf.c @@ -79,7 +79,7 @@ const char *extract_device_type_from_json(const json_t *dev) if (!dev) return NULL; - const json_t *type_val = json_object_get(dev, "d_type"); + const json_t *type_val = json_object_get(dev, "type"); if (!type_val || !json_is_string(type_val)) return NULL; @@ -246,7 +246,26 @@ static void extract_liquidctl_device_info(const json_t *dev, char *lcd_uid, } /** - * @brief Search for Liquidctl device in devices array. + * @brief Check if a Liquidctl device has an LCD display. + * @details Verifies that lcd_info exists in info.channels.lcd path. + */ +static int has_lcd_display(const json_t *dev) +{ + const json_t *lcd_info = get_lcd_info_from_device(dev); + if (!lcd_info) + return 0; + + const json_t *w = json_object_get(lcd_info, "screen_width"); + const json_t *h = json_object_get(lcd_info, "screen_height"); + if (!w || !h || !json_is_integer(w) || !json_is_integer(h)) + return 0; + + return (json_integer_value(w) > 0 && json_integer_value(h) > 0); +} + +/** + * @brief Search for Liquidctl device with LCD in devices array. + * @details Only selects Liquidctl devices that have a valid LCD display. */ static int search_liquidctl_device(const json_t *devices, char *lcd_uid, size_t uid_size, int *found_liquidctl, @@ -264,6 +283,15 @@ static int search_liquidctl_device(const json_t *devices, char *lcd_uid, if (!is_liquidctl_device(type_str)) continue; + if (!has_lcd_display(dev)) + { + const json_t *name_val = json_object_get(dev, "name"); + const char *name = name_val ? json_string_value(name_val) : "unknown"; + log_message(LOG_INFO, "Skipping Liquidctl device without LCD: %s", + name ? name : "unknown"); + continue; + } + extract_liquidctl_device_info(dev, lcd_uid, uid_size, found_liquidctl, screen_width, screen_height, device_name, name_size); From 5577e939181a84569327e09aaef04464f0b40353 Mon Sep 17 00:00:00 2001 From: damachine Date: Fri, 13 Feb 2026 22:43:07 +0100 Subject: [PATCH 4/8] ui: add wrong device detection for LCD support --- etc/coolercontrol/plugins/coolerdash/ui/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index ae0ac90..aa992f3 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -1397,8 +1397,11 @@

System Environment

for (const dev of devices) { const dtype = dev.type || ''; if (dtype === 'Liquidctl') { - found = dev; - break; + const lcdInfo = dev.info?.channels?.lcd?.lcd_info; + if (lcdInfo && lcdInfo.screen_width > 0 && lcdInfo.screen_height > 0) { + found = dev; + break; + } } } From daf951476f720e15e8b063e3ed2e0fd9e17c19f8 Mon Sep 17 00:00:00 2001 From: damachine Date: Sat, 14 Feb 2026 02:55:32 +0100 Subject: [PATCH 5/8] feat: integrate ALL CC sensors into Dashboard --- .../plugins/coolerdash/config.json | 74 +- .../plugins/coolerdash/ui/index.html | 1654 +++++++++++------ src/device/config.c | 455 +++-- src/device/config.h | 89 +- src/main.c | 9 +- src/mods/circle.c | 78 +- src/mods/display.c | 210 ++- src/mods/display.h | 85 +- src/mods/dual.c | 124 +- src/srv/cc_conf.c | 117 +- src/srv/cc_conf.h | 27 + src/srv/cc_sensor.c | 387 ++-- src/srv/cc_sensor.h | 110 +- 13 files changed, 2253 insertions(+), 1166 deletions(-) diff --git a/etc/coolercontrol/plugins/coolerdash/config.json b/etc/coolercontrol/plugins/coolerdash/config.json index 715b18a..62195fc 100644 --- a/etc/coolercontrol/plugins/coolerdash/config.json +++ b/etc/coolercontrol/plugins/coolerdash/config.json @@ -54,46 +54,46 @@ "size_labels": 30.0 }, - "cpu": { - "threshold_1": 55.0, - "threshold_2": 65.0, - "threshold_3": 75.0, - "max_scale": 115.0, - "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, - "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, - "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, - "threshold_4_color": { "r": 255, "g": 0, "b": 0 } - }, - - "gpu": { - "threshold_1": 55.0, - "threshold_2": 65.0, - "threshold_3": 75.0, - "max_scale": 115.0, - "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, - "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, - "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, - "threshold_4_color": { "r": 255, "g": 0, "b": 0 } - }, - - "liquid": { - "max_scale": 50.0, - "threshold_1": 25.0, - "threshold_2": 28.0, - "threshold_3": 31.0, - "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, - "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, - "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, - "threshold_4_color": { "r": 255, "g": 0, "b": 0 } + "sensors": { + "cpu": { + "threshold_1": 55.0, + "threshold_2": 65.0, + "threshold_3": 75.0, + "max_scale": 115.0, + "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, + "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, + "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, + "threshold_4_color": { "r": 255, "g": 0, "b": 0 }, + "offset_x": 0, + "offset_y": 0 + }, + "gpu": { + "threshold_1": 55.0, + "threshold_2": 65.0, + "threshold_3": 75.0, + "max_scale": 115.0, + "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, + "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, + "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, + "threshold_4_color": { "r": 255, "g": 0, "b": 0 }, + "offset_x": 0, + "offset_y": 0 + }, + "liquid": { + "threshold_1": 25.0, + "threshold_2": 28.0, + "threshold_3": 31.0, + "max_scale": 50.0, + "threshold_1_color": { "r": 0, "g": 255, "b": 0 }, + "threshold_2_color": { "r": 255, "g": 140, "b": 0 }, + "threshold_3_color": { "r": 255, "g": 70, "b": 0 }, + "threshold_4_color": { "r": 255, "g": 0, "b": 0 }, + "offset_x": 0, + "offset_y": 0 + } }, "positioning": { - "temp_offset_x_cpu": 0, - "temp_offset_x_gpu": 0, - "temp_offset_y_cpu": 0, - "temp_offset_y_gpu": 0, - "temp_offset_x_liquid": 0, - "temp_offset_y_liquid": 0, "degree_spacing": 16, "label_offset_x": 0, "label_offset_y": 0 diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index aa992f3..7a8effa 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -465,6 +465,88 @@ ::-webkit-scrollbar-track { background: var(--bg-primary); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--accent); } + + /* Sensor Config Sections */ + .sensor-config-section { + margin-bottom: 16px; + } + + .sensor-config-section:last-child { + margin-bottom: 0; + } + + /* Collapsible details (used in Sensors + Layout tabs) */ + .sensor-details, + .collapsible-details { + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + } + + .sensor-details summary, + .collapsible-details summary { + padding: 12px 16px; + background: var(--bg-secondary); + cursor: pointer; + font-weight: 600; + font-size: 15px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 8px; + user-select: none; + list-style: none; + border-bottom: 1px solid transparent; + transition: background 0.15s; + } + + .sensor-details summary::-webkit-details-marker, + .collapsible-details summary::-webkit-details-marker { display: none; } + + .sensor-details summary::before, + .collapsible-details summary::before { + content: '\25B6'; + font-size: 10px; + transition: transform 0.2s; + color: var(--text-dim); + } + + .sensor-details[open] summary::before, + .collapsible-details[open] summary::before { + transform: rotate(90deg); + } + + .sensor-details[open] summary, + .collapsible-details[open] summary { + border-bottom: 1px solid var(--border); + } + + .sensor-details summary:hover, + .collapsible-details summary:hover { + background: var(--bg-primary); + } + + .sensor-details-content, + .collapsible-details-content { + padding: 16px; + } + + .sensor-details .sub-section-title, + .collapsible-details .sub-section-title { + font-size: 13px; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 16px 0 8px 0; + padding-bottom: 4px; + border-bottom: 1px solid rgba(255,107,129,0.15); + } + + .sensor-details .sub-section-title:first-child, + .collapsible-details .sub-section-title:first-child { + margin-top: 0; + } @@ -472,9 +554,9 @@

CoolerDash Configuration

-

Customize your LCD temperature dashboard • Changes require plugin restart

+

Customize your LCD temperature dashboard • Changes require plugin restart

- 💡 Tip: After an update, click Reset to apply new default values. Feedback & bug reports are welcome — see the Info tab for links. + 💡 Tip: After an update, click Reset to apply new default values. Feedback & bug reports are welcome — see the Info tab for links.

@@ -484,13 +566,10 @@

CoolerDash Configuration

- - - - - - - + + + +
@@ -536,40 +615,47 @@

Display Mode

Sensor Slots

Sensor Assignment - Assign sensors to display positions. Use "None" to disable a slot. + Assign sensors to display positions. Use “None” to disable a slot. + Discovered sensors from CoolerControl API appear automatically.
Default: CPU - + + + + + +
Default: None (Circle only) - + + + + + +
Default: GPU - + + + + + +
@@ -578,7 +664,7 @@

Sensor Slots

-

Timing & Brightness

+

Timing & Brightness

@@ -600,12 +686,12 @@

Display Geometry

- Default: 0° + Default: 0°
@@ -651,99 +737,135 @@

Display Geometry

-

Bar Dimensions

-
-
- - Default: 24 - -
-
- - Default: 98 - -
- -
- - Default: 12 - -
-
- -

Individual Bar Heights

- Set to 0 to use default bar height -
-
- - Default: 0 - -
- -
- - Default: 0 - -
- -
- - Default: 0 - -
-
- -

Border & Margins

-
-
- - Default: On - -
- -
- - Default: 1.0 - +
+ Bar Dimensions +
+
+
+ + Default: 24 + +
+
+ + Default: 98 + +
+
+ + Default: 12 + +
+
- -
- - Default: 1 - +
+ +
+ Individual Bar Heights +
+ Set to 0 to use default bar height +
+
+ + Default: 0 + +
+
+ + Default: 0 + +
+
+ + Default: 0 + +
+
- -
- - Default: 1 - +
+ +
+ Border & Margins +
+
+
+ + Default: On + +
+
+ + Default: 1.0 + +
+
+ + Default: 1 + +
+
+ + Default: 1 + +
+
-
- -

Font

-
-
- - Default: Roboto Black - + + +
+ Font +
+
+
+ + Default: Roboto Black + +
+
+ + Default: 100 (per-sensor override in Sensors tab) + +
+
+ + Default: 30 + +
+
+
-
- - Default: 100 - +
+ Global Positioning +
+
+ Global Offsets + These offsets apply to all labels globally. Per-sensor value offsets are configured in the Sensors tab. +
+
+
+ + Default: 0 + +
+
+ + Default: 0 + +
+
+ + Default: 16 + +
+
+
-
- - Default: 30 - -
-
@@ -795,276 +917,21 @@

Text Colors

- -
-

CPU Thresholds

-
-
- - Default: 55 - -
- -
- - Default: 65 - -
- -
- - Default: 75 - -
- -
- - Default: 115 - -
-
- -

CPU Bar Colors

-
-
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
-
-
- - -
-

GPU Thresholds

-
-
- - Default: 55 - -
- -
- - Default: 65 - -
- -
- - Default: 75 - -
- -
- - Default: 115 - -
-
- -

GPU Bar Colors

-
-
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
-
-
- - -
-

Liquid Thresholds

-
-
- - Default: 25 - -
- -
- - Default: 28 - -
- -
- - Default: 31 - -
- -
- - Default: 50 - -
-
- -

Liquid Bar Colors

-
-
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
-
-
- - +
- Advanced Positioning - Value 0 = Automatic positioning. Adjust offsets to fine-tune element placement. -
- -

Upper Slot Position

-
-
- - Default: 0 - -
- -
- - Default: 0 - -
-
- -

Middle Slot Position

-
-
- - Default: 0 - -
- -
- - Default: 0 - -
+ Sensor Configuration + Configure thresholds, bar colors, and position offsets for each sensor assigned to a display slot. + New sensors are automatically added when assigned in the Display tab.
- -

Lower Slot Position

-
-
- - Default: 0 - -
- -
- - Default: 0 - -
-
- -

Labels Position

-
-
- - Default: 0 - -
- -
- - Default: 0 - -
- -
- - Default: 16 - -
+
+

+ Loading sensor configurations... +

- +

Image Paths

@@ -1074,17 +941,17 @@

Image Paths

-

Backup & Restore

+

Backup & Restore

Configuration Backup Export your current settings as a JSON file for backup or migration.
- +

Detected LCD Device

@@ -1104,32 +971,32 @@

Detected LCD Device

Device Name - +
Device UID - +
LCD Resolution - +
Display Shape - +
Firmware - +
Liquidctl Version - +
@@ -1139,14 +1006,14 @@

Detected LCD Device

- +

About CoolerDash

Plugin
-
CoolerDash v{{VERSION}} — LCD Temperature Dashboard
+
CoolerDash v{{VERSION}} — LCD Temperature Dashboard
Description
@@ -1183,7 +1050,7 @@

Community & Feedback

@@ -1192,11 +1059,11 @@

System Environment

Kernel - +
CoolerControl Version - +
@@ -1211,7 +1078,7 @@

System Environment

- Note: Changes are applied immediately — the plugin will be restarted automatically after Save or Reset. + Note: Changes are applied immediately — the plugin will be restarted automatically after Save or Reset.

@@ -1265,43 +1132,51 @@

System Environment

size_temp: 100.0, size_labels: 30.0 }, - cpu: { - threshold_1: 55.0, - threshold_2: 65.0, - threshold_3: 75.0, - max_scale: 115.0, - threshold_1_color: { r: 0, g: 255, b: 0 }, - threshold_2_color: { r: 255, g: 140, b: 0 }, - threshold_3_color: { r: 255, g: 70, b: 0 }, - threshold_4_color: { r: 255, g: 0, b: 0 } - }, - gpu: { - threshold_1: 55.0, - threshold_2: 65.0, - threshold_3: 75.0, - max_scale: 115.0, - threshold_1_color: { r: 0, g: 255, b: 0 }, - threshold_2_color: { r: 255, g: 140, b: 0 }, - threshold_3_color: { r: 255, g: 70, b: 0 }, - threshold_4_color: { r: 255, g: 0, b: 0 } - }, - liquid: { - max_scale: 50.0, - threshold_1: 25.0, - threshold_2: 28.0, - threshold_3: 31.0, - threshold_1_color: { r: 0, g: 255, b: 0 }, - threshold_2_color: { r: 255, g: 140, b: 0 }, - threshold_3_color: { r: 255, g: 70, b: 0 }, - threshold_4_color: { r: 255, g: 0, b: 0 } + sensors: { + cpu: { + threshold_1: 55.0, + threshold_2: 65.0, + threshold_3: 75.0, + max_scale: 115.0, + font_size_temp: 0, + label: '', + threshold_1_color: { r: 0, g: 255, b: 0 }, + threshold_2_color: { r: 255, g: 140, b: 0 }, + threshold_3_color: { r: 255, g: 70, b: 0 }, + threshold_4_color: { r: 255, g: 0, b: 0 }, + offset_x: 0, + offset_y: 0 + }, + gpu: { + threshold_1: 55.0, + threshold_2: 65.0, + threshold_3: 75.0, + max_scale: 115.0, + font_size_temp: 0, + label: '', + threshold_1_color: { r: 0, g: 255, b: 0 }, + threshold_2_color: { r: 255, g: 140, b: 0 }, + threshold_3_color: { r: 255, g: 70, b: 0 }, + threshold_4_color: { r: 255, g: 0, b: 0 }, + offset_x: 0, + offset_y: 0 + }, + liquid: { + threshold_1: 25.0, + threshold_2: 28.0, + threshold_3: 31.0, + max_scale: 50.0, + font_size_temp: 0, + label: '', + threshold_1_color: { r: 0, g: 255, b: 0 }, + threshold_2_color: { r: 255, g: 140, b: 0 }, + threshold_3_color: { r: 255, g: 70, b: 0 }, + threshold_4_color: { r: 255, g: 0, b: 0 }, + offset_x: 0, + offset_y: 0 + } }, positioning: { - temp_offset_x_cpu: 0, - temp_offset_x_gpu: 0, - temp_offset_y_cpu: 0, - temp_offset_y_gpu: 0, - temp_offset_x_liquid: 0, - temp_offset_y_liquid: 0, degree_spacing: 16, label_offset_x: 0, label_offset_y: 0 @@ -1310,6 +1185,8 @@

System Environment

let DEFAULT_CONFIG = null; let currentTab = 0; + let discoveredSensors = []; + let currentSensorConfig = {}; // ===== FALLBACK FUNCTIONS ===== const isInCoolerControl = window.parent !== window; @@ -1367,37 +1244,482 @@

System Environment

window.runPluginScript = function(mainFunction) { mainFunction(); }; } + // ===== SENSOR DISCOVERY ===== + async function discoverSensors(apiAddress, password) { + try { + const headers = { 'Content-Type': 'application/json' }; + if (password) { + headers['Authorization'] = 'Basic ' + btoa('admin:' + password); + } + + // GET /devices for device names + const devRes = await fetch(apiAddress + '/devices', { headers: password ? { 'Authorization': headers['Authorization'] } : {} }); + const devData = devRes.ok ? await devRes.json() : { devices: [] }; + const deviceNames = {}; + for (const dev of (devData.devices || [])) { + if (dev.uid) deviceNames[dev.uid] = dev.name || dev.uid; + } + + // POST /status for current sensor data + const statusRes = await fetch(apiAddress + '/status', { method: 'POST', headers, body: '{}' }); + if (!statusRes.ok) return; + const statusData = await statusRes.json(); + + const sensors = []; + for (const dev of (statusData.devices || [])) { + const uid = dev.uid || ''; + const devName = deviceNames[uid] || uid; + const lastStatus = Array.isArray(dev.status_history) && dev.status_history.length > 0 + ? dev.status_history[dev.status_history.length - 1] + : {}; + + // Temperature sensors + const temps = lastStatus.temps || []; + for (const t of temps) { + if (t.temp !== undefined && t.temp > -50 && t.temp < 200) { + sensors.push({ + id: uid + ':' + t.name, + label: devName + ' \u2014 ' + t.name, + device: devName, + name: t.name, + category: 'temp' + }); + } + } + + // Channel sensors (RPM, Duty, Watts, Freq) + const channels = lastStatus.channels || []; + for (const ch of channels) { + if (ch.rpm !== undefined) { + sensors.push({ + id: uid + ':' + ch.name + ' RPM', + label: devName + ' \u2014 ' + ch.name + ' RPM', + device: devName, + name: ch.name + ' RPM', + category: 'rpm' + }); + } + if (ch.duty !== undefined) { + sensors.push({ + id: uid + ':' + ch.name + ' Duty', + label: devName + ' \u2014 ' + ch.name + ' Duty', + device: devName, + name: ch.name + ' Duty', + category: 'duty' + }); + } + if (ch.watts !== undefined) { + sensors.push({ + id: uid + ':' + ch.name + ' Watts', + label: devName + ' \u2014 ' + ch.name + ' Watts', + device: devName, + name: ch.name + ' Watts', + category: 'watts' + }); + } + if (ch.freq !== undefined) { + sensors.push({ + id: uid + ':' + ch.name + ' Freq', + label: devName + ' \u2014 ' + ch.name + ' Freq', + device: devName, + name: ch.name + ' Freq', + category: 'freq' + }); + } + } + } + + discoveredSensors = sensors; + updateSensorSlotOptions(); + // Re-render sensor configs to update display names + if (Object.keys(currentSensorConfig).length > 0) { + renderSensorConfigs({ sensors: currentSensorConfig }); + } + } catch (error) { + console.warn('Sensor discovery failed:', error); + } + } + + function updateSensorSlotOptions() { + var selects = document.querySelectorAll('.sensor-slot-select'); + for (var i = 0; i < selects.length; i++) { + var select = selects[i]; + var currentValue = select.value; + + // Remove old dynamic optgroups (keep first "Standard" group) + var groups = select.querySelectorAll('optgroup'); + for (var g = groups.length - 1; g >= 1; g--) { + select.removeChild(groups[g]); + } + // Also remove any loose options outside optgroups + var looseOpts = select.querySelectorAll(':scope > option'); + for (var lo = 0; lo < looseOpts.length; lo++) { + select.removeChild(looseOpts[lo]); + } + + // Group discovered sensors by device + if (discoveredSensors.length > 0) { + var byDevice = {}; + for (var s = 0; s < discoveredSensors.length; s++) { + var sensor = discoveredSensors[s]; + if (!byDevice[sensor.device]) byDevice[sensor.device] = []; + byDevice[sensor.device].push(sensor); + } + for (var device in byDevice) { + if (!byDevice.hasOwnProperty(device)) continue; + var group = document.createElement('optgroup'); + group.label = device; + var deviceSensors = byDevice[device]; + deviceSensors.sort(function(a, b) { + return a.name.localeCompare(b.name); + }); + for (var ds = 0; ds < deviceSensors.length; ds++) { + var opt = document.createElement('option'); + opt.value = deviceSensors[ds].id; + opt.textContent = deviceSensors[ds].name; + group.appendChild(opt); + } + select.appendChild(group); + } + } + + // Restore selected value + if (currentValue) { + var exists = false; + for (var oi = 0; oi < select.options.length; oi++) { + if (select.options[oi].value === currentValue) { exists = true; break; } + } + if (exists) { + select.value = currentValue; + } else if (currentValue !== 'none' && currentValue !== '') { + // Add as custom option if not found (e.g. sensor not currently connected) + var customOpt = document.createElement('option'); + customOpt.value = currentValue; + customOpt.textContent = getSensorDisplayName(currentValue); + select.insertBefore(customOpt, select.firstChild); + select.value = currentValue; + } + } + } + } + + // ===== SENSOR CONFIG DEFAULTS ===== + function getDefaultSensorConfig(sensorId) { + var defaults = { + threshold_1: 55.0, threshold_2: 65.0, threshold_3: 75.0, max_scale: 115.0, + font_size_temp: 0, label: '', + threshold_1_color: { r: 0, g: 255, b: 0 }, + threshold_2_color: { r: 255, g: 140, b: 0 }, + threshold_3_color: { r: 255, g: 70, b: 0 }, + threshold_4_color: { r: 255, g: 0, b: 0 }, + offset_x: 0, offset_y: 0 + }; + + if (sensorId === 'liquid') { + defaults.threshold_1 = 25.0; + defaults.threshold_2 = 28.0; + defaults.threshold_3 = 31.0; + defaults.max_scale = 50.0; + } else if (/RPM|rpm/.test(sensorId)) { + defaults.threshold_1 = 500; + defaults.threshold_2 = 1000; + defaults.threshold_3 = 1500; + defaults.max_scale = 3000; + } else if (/Duty|duty/.test(sensorId)) { + defaults.threshold_1 = 25; + defaults.threshold_2 = 50; + defaults.threshold_3 = 75; + defaults.max_scale = 100; + } else if (/Watts|watts/.test(sensorId)) { + defaults.threshold_1 = 50; + defaults.threshold_2 = 100; + defaults.threshold_3 = 200; + defaults.max_scale = 500; + } else if (/Freq|freq/.test(sensorId)) { + defaults.threshold_1 = 1000; + defaults.threshold_2 = 2000; + defaults.threshold_3 = 3000; + defaults.max_scale = 6000; + } + + return defaults; + } + + function getSensorDisplayName(sensorId) { + var legacyNames = { cpu: 'CPU', gpu: 'GPU', liquid: 'Liquid' }; + if (legacyNames[sensorId]) return legacyNames[sensorId]; + var found = null; + for (var i = 0; i < discoveredSensors.length; i++) { + if (discoveredSensors[i].id === sensorId) { found = discoveredSensors[i]; break; } + } + if (found) return found.label; + // Fallback: show the raw ID + return sensorId; + } + + // ===== DYNAMIC SENSOR CONFIG UI ===== + function renderSensorConfigs(config) { + var container = document.getElementById('sensor-config-container'); + if (!container) return; + + var sensors = config.sensors || {}; + + // Ensure all discovered sensors have a config entry + for (var d = 0; d < discoveredSensors.length; d++) { + var dsId = discoveredSensors[d].id; + if (!sensors[dsId]) { + sensors[dsId] = getDefaultSensorConfig(dsId); + } + } + + var sensorIds = Object.keys(sensors); + + // Filter out dynamic sensors that duplicate a legacy sensor + // e.g. if "liquid" exists, skip any ":liquid" entry + var legacyNames = { cpu: true, gpu: true, liquid: true }; + sensorIds = sensorIds.filter(function(id) { + if (legacyNames[id]) return true; // keep legacy entries + var colonIdx = id.indexOf(':'); + if (colonIdx < 0) return true; // not dynamic + var sensorName = id.substring(colonIdx + 1).toLowerCase(); + // Skip if a legacy key matches the sensor name + if (sensorName === 'liquid' && sensors['liquid']) return false; + if (sensorName === 'cpu' && sensors['cpu']) return false; + if (sensorName === 'gpu' && sensors['gpu']) return false; + return true; + }); + + // Sort: cpu/gpu/liquid always first, then alphabetically by display name + var legacyOrder = { cpu: 0, gpu: 1, liquid: 2 }; + sensorIds.sort(function(a, b) { + var aLegacy = legacyOrder.hasOwnProperty(a) ? legacyOrder[a] : 99; + var bLegacy = legacyOrder.hasOwnProperty(b) ? legacyOrder[b] : 99; + if (aLegacy !== bLegacy) return aLegacy - bLegacy; + return getSensorDisplayName(a).localeCompare(getSensorDisplayName(b)); + }); + + if (sensorIds.length === 0) { + container.innerHTML = '

No sensor configurations found. Assign sensors in the Display tab.

'; + return; + } + + container.innerHTML = ''; + + for (var si = 0; si < sensorIds.length; si++) { + var sensorId = sensorIds[si]; + var sc = sensors[sensorId]; + var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_'); + var displayName = getSensorDisplayName(sensorId); + var defs = getDefaultSensorConfig(sensorId); + + var section = document.createElement('div'); + section.className = 'sensor-config-section'; + section.setAttribute('data-sensor-id', sensorId); + + section.innerHTML = + '
' + + '' + displayName + '' + + '
' + + + // Display section + '
Display
' + + '
' + + '
' + + '' + + 'Custom label (empty = auto)' + + '' + + '
' + + '
' + + '' + + 'Default: 100 (0 = global)' + + '' + + '
' + + '
' + + '' + + 'Default: 0' + + '' + + '
' + + '
' + + '' + + 'Default: 0' + + '' + + '
' + + '
' + + + // Thresholds section + '
Thresholds
' + + '
' + + '
' + + '' + + 'Default: ' + defs.threshold_1 + '' + + '' + + '
' + + '
' + + '' + + 'Default: ' + defs.threshold_2 + '' + + '' + + '
' + + '
' + + '' + + 'Default: ' + defs.threshold_3 + '' + + '' + + '
' + + '
' + + '' + + 'Default: ' + defs.max_scale + '' + + '' + + '
' + + '
' + + + // Bar Colors section + '
Bar Colors
' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + + '
' + + '
'; + + container.appendChild(section); + + // Setup color pickers after DOM insertion + setupColorPicker('color_' + safeName + '_1', 'rgb_' + safeName + '_1', sc.threshold_1_color || defs.threshold_1_color); + setupColorPicker('color_' + safeName + '_2', 'rgb_' + safeName + '_2', sc.threshold_2_color || defs.threshold_2_color); + setupColorPicker('color_' + safeName + '_3', 'rgb_' + safeName + '_3', sc.threshold_3_color || defs.threshold_3_color); + setupColorPicker('color_' + safeName + '_4', 'rgb_' + safeName + '_4', sc.threshold_4_color || defs.threshold_4_color); + } + + // Store reference for buildConfig + currentSensorConfig = sensors; + } + + function ensureSensorConfigsForSlots() { + var slotIds = ['sensor_slot_up', 'sensor_slot_mid', 'sensor_slot_down']; + var activeSensors = []; + + for (var i = 0; i < slotIds.length; i++) { + var select = document.getElementById(slotIds[i]); + if (select && select.value !== 'none' && select.value !== '') { + if (activeSensors.indexOf(select.value) === -1) { + activeSensors.push(select.value); + } + } + } + + // Check if any sensor is missing from the config + var needsRebuild = false; + for (var s = 0; s < activeSensors.length; s++) { + if (!currentSensorConfig[activeSensors[s]]) { + needsRebuild = true; + break; + } + } + + if (needsRebuild) { + // Collect current values from DOM + var sensors = collectSensorConfigsFromDOM(); + // Add defaults for new sensors + for (var ns = 0; ns < activeSensors.length; ns++) { + if (!sensors[activeSensors[ns]]) { + sensors[activeSensors[ns]] = getDefaultSensorConfig(activeSensors[ns]); + } + } + currentSensorConfig = sensors; + renderSensorConfigs({ sensors: sensors }); + } + } + + function collectSensorConfigsFromDOM() { + var sensors = {}; + var sections = document.querySelectorAll('.sensor-config-section'); + for (var i = 0; i < sections.length; i++) { + var section = sections[i]; + var sensorId = section.getAttribute('data-sensor-id'); + if (!sensorId) continue; + + var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_'); + var sc = {}; + + // Threshold/offset fields + var fields = section.querySelectorAll('.sensor-field'); + for (var f = 0; f < fields.length; f++) { + var field = fields[f].getAttribute('data-field'); + if (field) { + var numVal = parseFloat(fields[f].value); + sc[field] = isNaN(numVal) ? fields[f].value : numVal; + } + } + + // Colors + sc.threshold_1_color = getColorFromPicker('color_' + safeName + '_1'); + sc.threshold_2_color = getColorFromPicker('color_' + safeName + '_2'); + sc.threshold_3_color = getColorFromPicker('color_' + safeName + '_3'); + sc.threshold_4_color = getColorFromPicker('color_' + safeName + '_4'); + + sensors[sensorId] = sc; + } + return sensors; + } + // ===== DEVICE DETECTION ===== - let lastApiAddress = ''; - let lastApiPassword = ''; + var lastApiAddress = ''; + var lastApiPassword = ''; async function fetchDeviceInfo(apiAddress, password) { lastApiAddress = apiAddress; lastApiPassword = password; - const loadingEl = document.getElementById('device-loading'); - const contentEl = document.getElementById('device-panel-content'); - const errorEl = document.getElementById('device-error'); + var loadingEl = document.getElementById('device-loading'); + var contentEl = document.getElementById('device-panel-content'); + var errorEl = document.getElementById('device-error'); loadingEl.style.display = 'block'; contentEl.style.display = 'none'; errorEl.style.display = 'none'; try { - const headers = {}; + var headers = {}; if (password) { - headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`; + headers['Authorization'] = 'Basic ' + btoa('admin:' + password); } - const response = await fetch(`${apiAddress}/devices`, { headers }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - - const devices = data.devices || []; - let found = null; - for (const dev of devices) { - const dtype = dev.type || ''; + var response = await fetch(apiAddress + '/devices', { headers: headers }); + if (!response.ok) throw new Error('HTTP ' + response.status); + var data = await response.json(); + + var devices = data.devices || []; + var found = null; + for (var i = 0; i < devices.length; i++) { + var dev = devices[i]; + var dtype = dev.type || ''; if (dtype === 'Liquidctl') { - const lcdInfo = dev.info?.channels?.lcd?.lcd_info; + var lcdInfo = dev.info && dev.info.channels && dev.info.channels.lcd && dev.info.channels.lcd.lcd_info; if (lcdInfo && lcdInfo.screen_width > 0 && lcdInfo.screen_height > 0) { found = dev; break; @@ -1408,37 +1730,34 @@

System Environment

loadingEl.style.display = 'none'; if (found) { - const name = found.name || 'Unknown Device'; - const uid = found.uid || '—'; + var name = found.name || 'Unknown Device'; + var uid = found.uid || '\u2014'; document.getElementById('device-name').textContent = name; document.getElementById('device-uid').textContent = uid; - // Firmware from lc_info - const firmware = found.lc_info?.firmware_version || '—'; + var firmware = (found.lc_info && found.lc_info.firmware_version) || '\u2014'; document.getElementById('device-firmware').textContent = firmware; - // Liquidctl version from driver_info - const liquidctlVer = found.info?.driver_info?.version || '—'; + var liquidctlVer = (found.info && found.info.driver_info && found.info.driver_info.version) || '\u2014'; document.getElementById('device-liquidctl').textContent = liquidctlVer; - // LCD info path: info.channels..lcd_info - let width = 0, height = 0; - const channels = found.info?.channels; + var width = 0, height = 0; + var channels = found.info && found.info.channels; if (channels) { - for (const ch of Object.values(channels)) { - if (ch.lcd_info) { - width = ch.lcd_info.screen_width || 0; - height = ch.lcd_info.screen_height || 0; + for (var chKey in channels) { + if (channels.hasOwnProperty(chKey) && channels[chKey].lcd_info) { + width = channels[chKey].lcd_info.screen_width || 0; + height = channels[chKey].lcd_info.screen_height || 0; break; } } } - const resEl = document.getElementById('device-resolution'); - const shapeEl = document.getElementById('device-shape'); + var resEl = document.getElementById('device-resolution'); + var shapeEl = document.getElementById('device-shape'); if (width > 0 && height > 0) { - resEl.textContent = `${width} × ${height} px`; - const isCircular = name.includes('Kraken') && (width > 240 || height > 240); + resEl.textContent = width + ' \u00D7 ' + height + ' px'; + var isCircular = name.includes('Kraken') && (width > 240 || height > 240); shapeEl.textContent = isCircular ? 'Circular' : 'Rectangular'; } else { resEl.textContent = 'Not available'; @@ -1449,12 +1768,12 @@

System Environment

' Connected'; contentEl.style.display = 'block'; } else { - document.getElementById('device-name').textContent = '—'; - document.getElementById('device-uid').textContent = '—'; - document.getElementById('device-resolution').textContent = '—'; - document.getElementById('device-shape').textContent = '—'; - document.getElementById('device-firmware').textContent = '—'; - document.getElementById('device-liquidctl').textContent = '—'; + document.getElementById('device-name').textContent = '\u2014'; + document.getElementById('device-uid').textContent = '\u2014'; + document.getElementById('device-resolution').textContent = '\u2014'; + document.getElementById('device-shape').textContent = '\u2014'; + document.getElementById('device-firmware').textContent = '\u2014'; + document.getElementById('device-liquidctl').textContent = '\u2014'; document.getElementById('device-status').innerHTML = ' No LCD device found'; contentEl.style.display = 'block'; @@ -1465,7 +1784,7 @@

System Environment

document.getElementById('device-status').innerHTML = ' Detection failed'; document.getElementById('device-error-text').textContent = - `Could not connect to CoolerControl API (${error.message}).`; + 'Could not connect to CoolerControl API (' + error.message + ').'; contentEl.style.display = 'block'; errorEl.style.display = 'block'; } @@ -1477,28 +1796,26 @@

System Environment

async function fetchSystemInfo(apiAddress, password) { try { - const headers = {}; + var headers = {}; if (password) { - headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`; + headers['Authorization'] = 'Basic ' + btoa('admin:' + password); } - // Fetch /health for CC version - const healthRes = await fetch(`${apiAddress}/health`, { headers }); + var healthRes = await fetch(apiAddress + '/health', { headers: headers }); if (healthRes.ok) { - const health = await healthRes.json(); - const ccVersion = health.details?.version || '—'; + var health = await healthRes.json(); + var ccVersion = (health.details && health.details.version) || '\u2014'; document.getElementById('sys-cc-version').textContent = ccVersion; } - // Fetch /devices to extract kernel from driver_info (drv_type 'Kernel') - const devRes = await fetch(`${apiAddress}/devices`, { headers }); + var devRes = await fetch(apiAddress + '/devices', { headers: headers }); if (devRes.ok) { - const data = await devRes.json(); - const devices = data.devices || []; - let kernel = ''; - for (const dev of devices) { - const drvType = dev.info?.driver_info?.drv_type; - const ver = dev.info?.driver_info?.version; + var data = await devRes.json(); + var devices = data.devices || []; + var kernel = ''; + for (var i = 0; i < devices.length; i++) { + var drvType = devices[i].info && devices[i].info.driver_info && devices[i].info.driver_info.drv_type; + var ver = devices[i].info && devices[i].info.driver_info && devices[i].info.driver_info.version; if (drvType === 'Kernel' && ver) { kernel = ver; break; @@ -1515,53 +1832,63 @@

System Environment

// ===== UI FUNCTIONS ===== function switchTab(index) { - document.querySelectorAll('.tab').forEach((tab, i) => tab.classList.toggle('active', i === index)); - document.querySelectorAll('.panel').forEach((panel, i) => panel.classList.toggle('active', i === index)); + var tabs = document.querySelectorAll('.tab'); + var panels = document.querySelectorAll('.panel'); + for (var i = 0; i < tabs.length; i++) { + tabs[i].classList.toggle('active', i === index); + } + for (var j = 0; j < panels.length; j++) { + panels[j].classList.toggle('active', j === index); + } currentTab = index; } function toggleCircleInterval() { - const mode = document.getElementById('display_mode').value; + var mode = document.getElementById('display_mode').value; document.getElementById('circle_interval_group').style.display = mode === 'circle' ? 'block' : 'none'; - const midSlotGroup = document.getElementById('sensor_slot_mid_group'); - const midBarHeightGroup = document.getElementById('bar_height_mid_group'); + var midSlotGroup = document.getElementById('sensor_slot_mid_group'); + var midBarHeightGroup = document.getElementById('bar_height_mid_group'); if (midSlotGroup) midSlotGroup.style.display = mode === 'circle' ? 'block' : 'none'; if (midBarHeightGroup) midBarHeightGroup.style.display = mode === 'circle' ? 'block' : 'none'; if (mode === 'dual') { - const midSlot = document.getElementById('sensor_slot_mid'); + var midSlot = document.getElementById('sensor_slot_mid'); if (midSlot) midSlot.value = 'none'; } validateSensorSlots(); } function validateSensorSlots() { - const mode = document.getElementById('display_mode').value; - const slotUp = document.getElementById('sensor_slot_up').value; - const slotMid = document.getElementById('sensor_slot_mid').value; - const slotDown = document.getElementById('sensor_slot_down').value; + var mode = document.getElementById('display_mode').value; + var slotUp = document.getElementById('sensor_slot_up').value; + var slotMid = document.getElementById('sensor_slot_mid').value; + var slotDown = document.getElementById('sensor_slot_down').value; - const warningDiv = document.getElementById('sensor_slot_warning'); - const warningText = document.getElementById('sensor_slot_warning_text'); - let warnings = []; + var warningDiv = document.getElementById('sensor_slot_warning'); + var warningText = document.getElementById('sensor_slot_warning_text'); + var warnings = []; - const activeSlots = []; + var activeSlots = []; if (slotUp !== 'none') activeSlots.push({ name: 'Upper', value: slotUp }); if (mode === 'circle' && slotMid !== 'none') activeSlots.push({ name: 'Middle', value: slotMid }); if (slotDown !== 'none') activeSlots.push({ name: 'Lower', value: slotDown }); if (activeSlots.length === 0) warnings.push('At least one sensor slot must be active.'); - const usedSensors = {}; - for (const slot of activeSlots) { - if (usedSensors[slot.value]) { - warnings.push(`Duplicate: "${slot.value}" used in ${usedSensors[slot.value]} and ${slot.name}.`); + var usedSensors = {}; + for (var i = 0; i < activeSlots.length; i++) { + if (usedSensors[activeSlots[i].value]) { + warnings.push('Duplicate: "' + activeSlots[i].value + '" used in ' + usedSensors[activeSlots[i].value] + ' and ' + activeSlots[i].name + '.'); } else { - usedSensors[slot.value] = slot.name; + usedSensors[activeSlots[i].value] = activeSlots[i].name; } } warningDiv.style.display = warnings.length > 0 ? 'block' : 'none'; warningText.textContent = warnings.join(' '); + + // Ensure sensor configs exist for all active sensors + ensureSensorConfigsForSlots(); + return warnings.length === 0; } @@ -1570,40 +1897,40 @@

System Environment

} function rgbToHex(rgb) { - const r = Math.round(rgb.r).toString(16).padStart(2, '0'); - const g = Math.round(rgb.g).toString(16).padStart(2, '0'); - const b = Math.round(rgb.b).toString(16).padStart(2, '0'); - return `#${r}${g}${b}`; + var r = Math.round(rgb.r).toString(16).padStart(2, '0'); + var g = Math.round(rgb.g).toString(16).padStart(2, '0'); + var b = Math.round(rgb.b).toString(16).padStart(2, '0'); + return '#' + r + g + b; } function hexToRgb(hex) { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } function updateColorRGB(colorId, rgbId) { - const rgb = hexToRgb(document.getElementById(colorId).value); - if (rgb) document.getElementById(rgbId).textContent = `RGB(${rgb.r}, ${rgb.g}, ${rgb.b})`; + var rgb = hexToRgb(document.getElementById(colorId).value); + if (rgb) document.getElementById(rgbId).textContent = 'RGB(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; } function setupColorPicker(colorId, rgbId, rgbObj) { - const colorInput = document.getElementById(colorId); + var colorInput = document.getElementById(colorId); if (rgbObj && colorInput) { colorInput.value = rgbToHex(rgbObj); - const rgbEl = document.getElementById(rgbId); - if (rgbEl) rgbEl.textContent = `RGB(${Math.round(rgbObj.r)}, ${Math.round(rgbObj.g)}, ${Math.round(rgbObj.b)})`; + var rgbEl = document.getElementById(rgbId); + if (rgbEl) rgbEl.textContent = 'RGB(' + Math.round(rgbObj.r) + ', ' + Math.round(rgbObj.g) + ', ' + Math.round(rgbObj.b) + ')'; } } function getColorFromPicker(colorId) { - const el = document.getElementById(colorId); + var el = document.getElementById(colorId); return el ? hexToRgb(el.value) : null; } async function loadDefaultConfig() { try { - const response = await fetch('config.json'); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + var response = await fetch('config.json'); + if (!response.ok) throw new Error('HTTP ' + response.status); DEFAULT_CONFIG = await response.json(); return DEFAULT_CONFIG; } catch (error) { @@ -1613,64 +1940,99 @@

System Environment

} } + // ===== BUILD CONFIG FROM FORM ===== function buildConfig() { - const form = document.getElementById('configForm'); - const formData = new FormData(form); - const config = { + var form = document.getElementById('configForm'); + var formData = new FormData(form); + var config = { daemon: {}, paths: {}, display: {}, layout: {}, colors: {}, - font: {}, cpu: {}, gpu: {}, liquid: {}, positioning: {} + font: {}, sensors: {}, positioning: {} }; - for (const [key, value] of formData.entries()) { - const [section, field] = key.split('.'); - if (section && field && config[section]) { - const numValue = parseFloat(value); - config[section][field] = isNaN(numValue) ? value : numValue; + var entries = formData.entries(); + var entry; + while (!(entry = entries.next()).done) { + var key = entry.value[0]; + var value = entry.value[1]; + var parts = key.split('.'); + if (parts.length === 2 && config[parts[0]] !== undefined) { + // Sensor slot values must always be strings + var isSlotField = (parts[1] === 'sensor_slot_up' || parts[1] === 'sensor_slot_mid' || parts[1] === 'sensor_slot_down'); + if (isSlotField) { + config[parts[0]][parts[1]] = value; + } else { + // Strict number check: only convert pure numeric strings + var trimmed = value.trim(); + var isNumeric = trimmed !== '' && /^-?\d+(\.\d+)?$/.test(trimmed); + config[parts[0]][parts[1]] = isNumeric ? parseFloat(trimmed) : value; + } } } - // Colors + // Global colors config.colors.display_background = getColorFromPicker('color_display_background'); config.colors.bar_background = getColorFromPicker('color_bar_background'); config.colors.bar_border = getColorFromPicker('color_bar_border'); config.colors.font_temp = getColorFromPicker('color_font_temp'); config.colors.font_label = getColorFromPicker('color_font_label'); - // CPU colors - config.cpu.threshold_1_color = getColorFromPicker('color_cpu_1'); - config.cpu.threshold_2_color = getColorFromPicker('color_cpu_2'); - config.cpu.threshold_3_color = getColorFromPicker('color_cpu_3'); - config.cpu.threshold_4_color = getColorFromPicker('color_cpu_4'); - - // GPU colors - config.gpu.threshold_1_color = getColorFromPicker('color_gpu_1'); - config.gpu.threshold_2_color = getColorFromPicker('color_gpu_2'); - config.gpu.threshold_3_color = getColorFromPicker('color_gpu_3'); - config.gpu.threshold_4_color = getColorFromPicker('color_gpu_4'); - - // Liquid colors - config.liquid.threshold_1_color = getColorFromPicker('color_liquid_1'); - config.liquid.threshold_2_color = getColorFromPicker('color_liquid_2'); - config.liquid.threshold_3_color = getColorFromPicker('color_liquid_3'); - config.liquid.threshold_4_color = getColorFromPicker('color_liquid_4'); + // Sensor configs from dynamic form sections + config.sensors = collectSensorConfigsFromDOM(); return config; } + // ===== POPULATE FORM FROM CONFIG ===== function populateForm(config) { - for (const [section, fields] of Object.entries(config)) { + // Standard form fields (non-sensor, non-color) + for (var section in config) { + if (!config.hasOwnProperty(section)) continue; + if (section === 'sensors' || section === 'colors') continue; + var fields = config[section]; if (typeof fields === 'object' && fields !== null) { - for (const [field, value] of Object.entries(fields)) { - const input = document.querySelector(`[name="${section}.${field}"]`); + for (var field in fields) { + if (!fields.hasOwnProperty(field)) continue; + var value = fields[field]; + var input = document.querySelector('[name="' + section + '.' + field + '"]'); if (input && typeof value !== 'object') { - input.value = typeof value === 'boolean' ? (value ? "1" : "0") : value; + input.value = (typeof value === 'boolean') ? (value ? "1" : "0") : value; if (field === 'brightness') updateRangeValue('brightness', value); } } } } - // Colors + // Sensor slot selects: ensure custom values are available as options + var slotFields = ['sensor_slot_up', 'sensor_slot_mid', 'sensor_slot_down']; + var slotDefaults = { sensor_slot_up: 'cpu', sensor_slot_mid: 'liquid', sensor_slot_down: 'gpu' }; + for (var sf = 0; sf < slotFields.length; sf++) { + var slotValue = config.display && config.display[slotFields[sf]]; + if (slotValue) { + // Ensure value is a string (fix legacy numeric corruption) + slotValue = String(slotValue); + // Reject pure numeric values (corrupt from old parseFloat bug) + if (/^\d+$/.test(slotValue)) { + slotValue = slotDefaults[slotFields[sf]] || 'cpu'; + config.display[slotFields[sf]] = slotValue; + } + var select = document.getElementById(slotFields[sf]); + if (select) { + var exists = false; + for (var oi = 0; oi < select.options.length; oi++) { + if (select.options[oi].value === slotValue) { exists = true; break; } + } + if (!exists && slotValue !== 'none') { + var opt = document.createElement('option'); + opt.value = slotValue; + opt.textContent = getSensorDisplayName(slotValue); + select.insertBefore(opt, select.firstChild); + } + select.value = slotValue; + } + } + } + + // Global colors if (config.colors) { setupColorPicker('color_display_background', 'rgb_display_background', config.colors.display_background); setupColorPicker('color_bar_background', 'rgb_bar_background', config.colors.bar_background); @@ -1679,47 +2041,28 @@

System Environment

setupColorPicker('color_font_label', 'rgb_font_label', config.colors.font_label); } - // CPU colors - if (config.cpu) { - setupColorPicker('color_cpu_1', 'rgb_cpu_1', config.cpu.threshold_1_color); - setupColorPicker('color_cpu_2', 'rgb_cpu_2', config.cpu.threshold_2_color); - setupColorPicker('color_cpu_3', 'rgb_cpu_3', config.cpu.threshold_3_color); - setupColorPicker('color_cpu_4', 'rgb_cpu_4', config.cpu.threshold_4_color); - } - - // GPU colors - if (config.gpu) { - setupColorPicker('color_gpu_1', 'rgb_gpu_1', config.gpu.threshold_1_color); - setupColorPicker('color_gpu_2', 'rgb_gpu_2', config.gpu.threshold_2_color); - setupColorPicker('color_gpu_3', 'rgb_gpu_3', config.gpu.threshold_3_color); - setupColorPicker('color_gpu_4', 'rgb_gpu_4', config.gpu.threshold_4_color); - } - - // Liquid colors - if (config.liquid) { - setupColorPicker('color_liquid_1', 'rgb_liquid_1', config.liquid.threshold_1_color); - setupColorPicker('color_liquid_2', 'rgb_liquid_2', config.liquid.threshold_2_color); - setupColorPicker('color_liquid_3', 'rgb_liquid_3', config.liquid.threshold_3_color); - setupColorPicker('color_liquid_4', 'rgb_liquid_4', config.liquid.threshold_4_color); - } + // Render dynamic sensor configs + currentSensorConfig = config.sensors || {}; + renderSensorConfigs(config); toggleCircleInterval(); validateSensorSlots(); } + // ===== SAVE / RESET / EXPORT ===== async function saveConfig() { if (!validateSensorSlots()) { alert("Please fix sensor slot warnings before saving."); return; } - const config = buildConfig(); + var config = buildConfig(); try { await writeConfigToFile(config); requestRestartAfterSave(1000); await savePluginConfig(config); alert("Configuration saved! Plugin will restart."); - setTimeout(() => window.close(), 300); + setTimeout(function() { window.close(); }, 300); } catch (error) { console.error("Save failed:", error); alert("Save failed: " + error.message); @@ -1728,14 +2071,14 @@

System Environment

function exportConfig() { try { - const config = buildConfig(); - const jsonStr = JSON.stringify(config, null, 2); - const blob = new Blob([jsonStr], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + var config = buildConfig(); + var jsonStr = JSON.stringify(config, null, 2); + var blob = new Blob([jsonStr], { type: 'application/json' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); a.href = url; - const date = new Date().toISOString().slice(0, 10); - a.download = `coolerdash-config-backup-${date}.json`; + var date = new Date().toISOString().slice(0, 10); + a.download = 'coolerdash-config-backup-' + date + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -1748,20 +2091,21 @@

System Environment

async function restartPluginDaemon(config) { try { - const apiAddress = config.daemon?.address || 'http://localhost:11987'; - const password = config.daemon?.password || ''; - const headers = { 'Content-Type': 'application/json' }; - if (password) headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`; + var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987'; + var password = (config.daemon && config.daemon.password) || ''; + var headers = { 'Content-Type': 'application/json' }; + if (password) headers['Authorization'] = 'Basic ' + btoa('admin:' + password); - const response = await fetch(`${apiAddress}/plugins/coolerdash/restart`, { method: 'POST', headers }); + var response = await fetch(apiAddress + '/plugins/coolerdash/restart', { method: 'POST', headers: headers }); if (!response.ok) console.warn("Could not restart plugin automatically"); } catch (error) { console.error("Plugin restart failed:", error); } } - function requestRestartAfterSave(delayMs = 800) { - const doRestart = () => { + function requestRestartAfterSave(delayMs) { + delayMs = delayMs || 800; + var doRestart = function() { try { if (typeof restart === 'function') restart(); else restartPluginDaemon(buildConfig()); @@ -1769,10 +2113,10 @@

System Environment

}; if (typeof successfulConfigSaveCallback === 'function') { - successfulConfigSaveCallback(() => setTimeout(doRestart, delayMs)); + successfulConfigSaveCallback(function() { setTimeout(doRestart, delayMs); }); } else { - const handler = (event) => { - if (event.data?.type === 'configSaved') { + var handler = function(event) { + if (event.data && event.data.type === 'configSaved') { window.removeEventListener('message', handler); setTimeout(doRestart, delayMs); } @@ -1783,20 +2127,20 @@

System Environment

async function writeConfigToFile(config) { try { - const apiAddress = config.daemon?.address || 'http://localhost:11987'; - const password = config.daemon?.password || ''; + var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987'; + var password = (config.daemon && config.daemon.password) || ''; if (password) { - const response = await fetch(`${apiAddress}/plugins/coolerdash/config`, { + var response = await fetch(apiAddress + '/plugins/coolerdash/config', { method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${btoa(`admin:${password}`)}` }, + headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + btoa('admin:' + password) }, body: JSON.stringify(config, null, 2) }); if (response.ok) return true; } - const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); - const a = document.createElement('a'); + var blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'config.json'; a.click(); @@ -1815,43 +2159,119 @@

System Environment

requestRestartAfterSave(1000); await savePluginConfig(FACTORY_DEFAULTS); alert('Reset to factory defaults! Plugin will restart.'); - setTimeout(() => window.close(), 300); + setTimeout(function() { window.close(); }, 300); } catch (error) { console.error("Reset failed:", error); alert('Reset failed: ' + error.message); } } - // Initialize - runPluginScript(async () => { + // ===== LEGACY CONFIG MIGRATION ===== + function migrateConfig(config) { + var sensors = config.sensors || {}; + var needsMigration = false; + + // Detect legacy format: top-level cpu/gpu/liquid sections + if (!config.sensors && (config.cpu || config.gpu || config.liquid)) { + needsMigration = true; + if (config.cpu) sensors.cpu = config.cpu; + if (config.gpu) sensors.gpu = config.gpu; + if (config.liquid) sensors.liquid = config.liquid; + + // Migrate per-sensor positioning offsets + if (config.positioning) { + if (sensors.cpu) { + sensors.cpu.offset_x = config.positioning.temp_offset_x_cpu || 0; + sensors.cpu.offset_y = config.positioning.temp_offset_y_cpu || 0; + } + if (sensors.gpu) { + sensors.gpu.offset_x = config.positioning.temp_offset_x_gpu || 0; + sensors.gpu.offset_y = config.positioning.temp_offset_y_gpu || 0; + } + if (sensors.liquid) { + sensors.liquid.offset_x = config.positioning.temp_offset_x_liquid || 0; + sensors.liquid.offset_y = config.positioning.temp_offset_y_liquid || 0; + } + } + } + + // Build merged config + var merged = { + daemon: Object.assign({}, DEFAULT_CONFIG.daemon, config.daemon || {}), + paths: Object.assign({}, DEFAULT_CONFIG.paths, config.paths || {}), + display: Object.assign({}, DEFAULT_CONFIG.display, config.display || {}), + layout: Object.assign({}, DEFAULT_CONFIG.layout, config.layout || {}), + colors: Object.assign({}, DEFAULT_CONFIG.colors, config.colors || {}), + font: Object.assign({}, DEFAULT_CONFIG.font, config.font || {}), + sensors: Object.assign({}, DEFAULT_CONFIG.sensors || {}), + positioning: Object.assign({}, DEFAULT_CONFIG.positioning, config.positioning || {}) + }; + + // Deep merge sensor configs (skip corrupt numeric keys) + for (var id in sensors) { + if (!sensors.hasOwnProperty(id)) continue; + // Remove corrupt numeric sensor IDs from old parseFloat bug + if (/^\d+$/.test(id)) continue; + var defaults = merged.sensors[id] || getDefaultSensorConfig(id); + merged.sensors[id] = Object.assign({}, defaults, sensors[id]); + // Deep merge color objects + var colorKeys = ['threshold_1_color', 'threshold_2_color', 'threshold_3_color', 'threshold_4_color']; + for (var c = 0; c < colorKeys.length; c++) { + if (sensors[id][colorKeys[c]]) { + merged.sensors[id][colorKeys[c]] = Object.assign({}, defaults[colorKeys[c]] || {}, sensors[id][colorKeys[c]]); + } + } + } + + // Sanitize corrupt numeric slot values (from old parseFloat bug) + var slotDefaults = { sensor_slot_up: 'cpu', sensor_slot_mid: 'liquid', sensor_slot_down: 'gpu' }; + for (var slotKey in slotDefaults) { + if (merged.display && merged.display[slotKey] !== undefined) { + var sv = String(merged.display[slotKey]); + if (/^\d+$/.test(sv)) { + merged.display[slotKey] = slotDefaults[slotKey]; + } else { + merged.display[slotKey] = sv; + } + } + } + + // Clean up legacy positioning keys + delete merged.positioning.temp_offset_x_cpu; + delete merged.positioning.temp_offset_x_gpu; + delete merged.positioning.temp_offset_y_cpu; + delete merged.positioning.temp_offset_y_gpu; + delete merged.positioning.temp_offset_x_liquid; + delete merged.positioning.temp_offset_y_liquid; + + // Remove legacy top-level sensor sections + delete merged.cpu; + delete merged.gpu; + delete merged.liquid; + + return merged; + } + + // ===== INITIALIZATION ===== + runPluginScript(async function() { try { await loadDefaultConfig(); - let config = await getPluginConfig(); + var config = await getPluginConfig(); if (!config || Object.keys(config).length === 0) { - config = DEFAULT_CONFIG; + config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); } else { - config = { - daemon: { ...DEFAULT_CONFIG.daemon, ...(config.daemon || {}) }, - paths: { ...DEFAULT_CONFIG.paths, ...(config.paths || {}) }, - display: { ...DEFAULT_CONFIG.display, ...(config.display || {}) }, - layout: { ...DEFAULT_CONFIG.layout, ...(config.layout || {}) }, - colors: { ...DEFAULT_CONFIG.colors, ...(config.colors || {}) }, - font: { ...DEFAULT_CONFIG.font, ...(config.font || {}) }, - cpu: { ...DEFAULT_CONFIG.cpu, ...(config.cpu || {}) }, - gpu: { ...DEFAULT_CONFIG.gpu, ...(config.gpu || {}) }, - liquid: { ...DEFAULT_CONFIG.liquid, ...(config.liquid || {}) }, - positioning: { ...DEFAULT_CONFIG.positioning, ...(config.positioning || {}) } - }; + config = migrateConfig(config); } populateForm(config); - // Detect device from CoolerControl API - const apiAddr = config.daemon?.address || 'http://localhost:11987'; - const apiPass = config.daemon?.password || ''; + // Detect device and discover sensors from CoolerControl API + var apiAddr = (config.daemon && config.daemon.address) || 'http://localhost:11987'; + var apiPass = (config.daemon && config.daemon.password) || ''; fetchDeviceInfo(apiAddr, apiPass); fetchSystemInfo(apiAddr, apiPass); + discoverSensors(apiAddr, apiPass); } catch (error) { console.error("Init failed:", error); if (DEFAULT_CONFIG) populateForm(DEFAULT_CONFIG); diff --git a/src/device/config.c b/src/device/config.c index 5fcb627..464615d 100644 --- a/src/device/config.c +++ b/src/device/config.c @@ -237,39 +237,144 @@ static void set_font_defaults(Config *config) } /** - * @brief Set temperature defaults. + * @brief Find or create a SensorConfig entry by sensor_id. + * @return Pointer to entry, or NULL if array is full */ -static void set_temperature_defaults(Config *config) +static SensorConfig *ensure_sensor_config(Config *config, const char *sensor_id) { - // CPU temperature defaults - if (config->temp_cpu_threshold_1 == 0.0f) - config->temp_cpu_threshold_1 = 55.0f; - if (config->temp_cpu_threshold_2 == 0.0f) - config->temp_cpu_threshold_2 = 65.0f; - if (config->temp_cpu_threshold_3 == 0.0f) - config->temp_cpu_threshold_3 = 75.0f; - if (config->temp_cpu_max_scale == 0.0f) - config->temp_cpu_max_scale = 115.0f; - - // GPU temperature defaults (same as CPU) - if (config->temp_gpu_threshold_1 == 0.0f) - config->temp_gpu_threshold_1 = 55.0f; - if (config->temp_gpu_threshold_2 == 0.0f) - config->temp_gpu_threshold_2 = 65.0f; - if (config->temp_gpu_threshold_3 == 0.0f) - config->temp_gpu_threshold_3 = 75.0f; - if (config->temp_gpu_max_scale == 0.0f) - config->temp_gpu_max_scale = 115.0f; - - // Liquid temperature defaults - if (config->temp_liquid_threshold_1 == 0.0f) - config->temp_liquid_threshold_1 = 25.0f; - if (config->temp_liquid_threshold_2 == 0.0f) - config->temp_liquid_threshold_2 = 28.0f; - if (config->temp_liquid_threshold_3 == 0.0f) - config->temp_liquid_threshold_3 = 31.0f; - if (config->temp_liquid_max_scale == 0.0f) - config->temp_liquid_max_scale = 50.0f; + for (int i = 0; i < config->sensor_config_count; i++) + { + if (strcmp(config->sensor_configs[i].sensor_id, sensor_id) == 0) + return &config->sensor_configs[i]; + } + if (config->sensor_config_count >= MAX_SENSOR_CONFIGS) + return NULL; + SensorConfig *sc = &config->sensor_configs[config->sensor_config_count++]; + memset(sc, 0, sizeof(SensorConfig)); + cc_safe_strcpy(sc->sensor_id, sizeof(sc->sensor_id), sensor_id); + return sc; +} + +/** + * @brief Initialize a SensorConfig with defaults for a given category. + */ +void init_default_sensor_config(SensorConfig *sc, const char *sensor_id, + int category) +{ + if (!sc || !sensor_id) + return; + memset(sc, 0, sizeof(SensorConfig)); + cc_safe_strcpy(sc->sensor_id, sizeof(sc->sensor_id), sensor_id); + + switch (category) + { + case 0: /* SENSOR_CATEGORY_TEMP */ + sc->threshold_1 = 55.0f; + sc->threshold_2 = 65.0f; + sc->threshold_3 = 75.0f; + sc->max_scale = 115.0f; + break; + case 1: /* SENSOR_CATEGORY_RPM */ + sc->threshold_1 = 500.0f; + sc->threshold_2 = 1000.0f; + sc->threshold_3 = 1500.0f; + sc->max_scale = 3000.0f; + break; + case 2: /* SENSOR_CATEGORY_DUTY */ + sc->threshold_1 = 25.0f; + sc->threshold_2 = 50.0f; + sc->threshold_3 = 75.0f; + sc->max_scale = 100.0f; + break; + case 3: /* SENSOR_CATEGORY_WATTS */ + sc->threshold_1 = 50.0f; + sc->threshold_2 = 100.0f; + sc->threshold_3 = 200.0f; + sc->max_scale = 500.0f; + break; + case 4: /* SENSOR_CATEGORY_FREQ */ + sc->threshold_1 = 1000.0f; + sc->threshold_2 = 2000.0f; + sc->threshold_3 = 3000.0f; + sc->max_scale = 6000.0f; + break; + default: + sc->threshold_1 = 55.0f; + sc->threshold_2 = 65.0f; + sc->threshold_3 = 75.0f; + sc->max_scale = 115.0f; + break; + } + + sc->threshold_1_bar = (Color){0, 255, 0, 1}; + sc->threshold_2_bar = (Color){255, 140, 0, 1}; + sc->threshold_3_bar = (Color){255, 70, 0, 1}; + sc->threshold_4_bar = (Color){255, 0, 0, 1}; +} + +/** + * @brief Find sensor configuration by sensor ID (public). + */ +const SensorConfig *get_sensor_config(const Config *config, + const char *sensor_id) +{ + if (!config || !sensor_id) + return NULL; + for (int i = 0; i < config->sensor_config_count; i++) + { + if (strcmp(config->sensor_configs[i].sensor_id, sensor_id) == 0) + return &config->sensor_configs[i]; + } + return NULL; +} + +/** + * @brief Set default sensor configurations. + * @details Ensures cpu, gpu, liquid configs exist with proper threshold defaults. + */ +static void set_default_sensor_configs(Config *config) +{ + /* Ensure CPU config */ + SensorConfig *cpu = ensure_sensor_config(config, "cpu"); + if (cpu) + { + if (cpu->threshold_1 == 0.0f) + cpu->threshold_1 = 55.0f; + if (cpu->threshold_2 == 0.0f) + cpu->threshold_2 = 65.0f; + if (cpu->threshold_3 == 0.0f) + cpu->threshold_3 = 75.0f; + if (cpu->max_scale == 0.0f) + cpu->max_scale = 115.0f; + } + + /* Ensure GPU config */ + SensorConfig *gpu = ensure_sensor_config(config, "gpu"); + if (gpu) + { + if (gpu->threshold_1 == 0.0f) + gpu->threshold_1 = 55.0f; + if (gpu->threshold_2 == 0.0f) + gpu->threshold_2 = 65.0f; + if (gpu->threshold_3 == 0.0f) + gpu->threshold_3 = 75.0f; + if (gpu->max_scale == 0.0f) + gpu->max_scale = 115.0f; + } + + /* Ensure Liquid config (lower thresholds) */ + SensorConfig *liquid = ensure_sensor_config(config, "liquid"); + if (liquid) + { + if (liquid->threshold_1 == 0.0f) + liquid->threshold_1 = 25.0f; + if (liquid->threshold_2 == 0.0f) + liquid->threshold_2 = 28.0f; + if (liquid->threshold_3 == 0.0f) + liquid->threshold_3 = 31.0f; + if (liquid->max_scale == 0.0f) + liquid->max_scale = 50.0f; + } } /** @@ -295,27 +400,13 @@ typedef struct */ static void set_color_defaults(Config *config) { + /* Global color defaults */ ColorDefault color_defaults[] = { - {&config->display_background_color, 0, 0, 0}, // Main background (black) + {&config->display_background_color, 0, 0, 0}, {&config->layout_bar_color_background, 52, 52, 52}, {&config->layout_bar_color_border, 192, 192, 192}, {&config->font_color_temp, 255, 255, 255}, - {&config->font_color_label, 200, 200, 200}, - // CPU temperature colors - {&config->temp_cpu_threshold_1_bar, 0, 255, 0}, - {&config->temp_cpu_threshold_2_bar, 255, 140, 0}, - {&config->temp_cpu_threshold_3_bar, 255, 70, 0}, - {&config->temp_cpu_threshold_4_bar, 255, 0, 0}, - // GPU temperature colors (same as CPU) - {&config->temp_gpu_threshold_1_bar, 0, 255, 0}, - {&config->temp_gpu_threshold_2_bar, 255, 140, 0}, - {&config->temp_gpu_threshold_3_bar, 255, 70, 0}, - {&config->temp_gpu_threshold_4_bar, 255, 0, 0}, - // Liquid temperature colors - {&config->temp_liquid_threshold_1_bar, 0, 255, 0}, - {&config->temp_liquid_threshold_2_bar, 255, 140, 0}, - {&config->temp_liquid_threshold_3_bar, 255, 70, 0}, - {&config->temp_liquid_threshold_4_bar, 255, 0, 0}}; + {&config->font_color_label, 200, 200, 200}}; const size_t color_count = sizeof(color_defaults) / sizeof(color_defaults[0]); for (size_t i = 0; i < color_count; i++) @@ -327,19 +418,44 @@ static void set_color_defaults(Config *config) color_defaults[i].color_ptr->b = color_defaults[i].b; } } + + /* Per-sensor color defaults (apply to all sensor configs) */ + for (int i = 0; i < config->sensor_config_count; i++) + { + SensorConfig *sc = &config->sensor_configs[i]; + if (is_color_unset(&sc->threshold_1_bar)) + sc->threshold_1_bar = (Color){0, 255, 0, 1}; + if (is_color_unset(&sc->threshold_2_bar)) + sc->threshold_2_bar = (Color){255, 140, 0, 1}; + if (is_color_unset(&sc->threshold_3_bar)) + sc->threshold_3_bar = (Color){255, 70, 0, 1}; + if (is_color_unset(&sc->threshold_4_bar)) + sc->threshold_4_bar = (Color){255, 0, 0, 1}; + } } /** * @brief Check if a sensor slot value is valid. + * @details Accepts legacy ("cpu","gpu","liquid","none") and dynamic ("uid:name"). */ static int is_valid_sensor_slot(const char *slot) { if (!slot || slot[0] == '\0') return 0; - return (strcmp(slot, "cpu") == 0 || - strcmp(slot, "gpu") == 0 || - strcmp(slot, "liquid") == 0 || - strcmp(slot, "none") == 0); + + /* Legacy values */ + if (strcmp(slot, "cpu") == 0 || + strcmp(slot, "gpu") == 0 || + strcmp(slot, "liquid") == 0 || + strcmp(slot, "none") == 0) + return 1; + + /* Dynamic format: "uid:sensor_name" */ + const char *sep = strchr(slot, ':'); + if (sep && sep != slot && *(sep + 1) != '\0') + return 1; + + return 0; } /** @@ -439,7 +555,7 @@ static void apply_system_defaults(Config *config) set_display_defaults(config); set_layout_defaults(config); set_font_defaults(config); - set_temperature_defaults(config); + set_default_sensor_configs(config); set_color_defaults(config); validate_sensor_slots(config); } @@ -677,6 +793,11 @@ static void load_display_from_json(json_t *root, Config *config) if (value) cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), value); } + else if (slot_up && !json_is_string(slot_up)) + { + log_message(LOG_WARNING, "sensor_slot_up has non-string type, using default '%s'", + config->sensor_slot_up); + } json_t *slot_mid = json_object_get(display, "sensor_slot_mid"); if (slot_mid && json_is_string(slot_mid)) @@ -685,6 +806,11 @@ static void load_display_from_json(json_t *root, Config *config) if (value) cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), value); } + else if (slot_mid && !json_is_string(slot_mid)) + { + log_message(LOG_WARNING, "sensor_slot_mid has non-string type, using default '%s'", + config->sensor_slot_mid); + } json_t *slot_down = json_object_get(display, "sensor_slot_down"); if (slot_down && json_is_string(slot_down)) @@ -693,6 +819,11 @@ static void load_display_from_json(json_t *root, Config *config) if (value) cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), value); } + else if (slot_down && !json_is_string(slot_down)) + { + log_message(LOG_WARNING, "sensor_slot_down has non-string type, using default '%s'", + config->sensor_slot_down); + } } /** @@ -837,124 +968,136 @@ static void load_font_from_json(json_t *root, Config *config) } /** - * @brief Load CPU temperature settings from JSON. + * @brief Load a single sensor config entry from a JSON object. + * @details Generic loader for thresholds, max_scale, colors, and offsets. */ -static void load_cpu_temperature_from_json(json_t *root, Config *config) +static void load_sensor_config_from_json(json_t *obj, SensorConfig *sc) { - json_t *cpu = json_object_get(root, "cpu"); - if (!cpu || !json_is_object(cpu)) + if (!obj || !json_is_object(obj) || !sc) return; - json_t *threshold_1 = json_object_get(cpu, "threshold_1"); - if (threshold_1 && json_is_number(threshold_1)) - { - config->temp_cpu_threshold_1 = (float)json_number_value(threshold_1); - } + json_t *val; - json_t *threshold_2 = json_object_get(cpu, "threshold_2"); - if (threshold_2 && json_is_number(threshold_2)) - { - config->temp_cpu_threshold_2 = (float)json_number_value(threshold_2); - } + val = json_object_get(obj, "threshold_1"); + if (val && json_is_number(val)) + sc->threshold_1 = (float)json_number_value(val); - json_t *threshold_3 = json_object_get(cpu, "threshold_3"); - if (threshold_3 && json_is_number(threshold_3)) - { - config->temp_cpu_threshold_3 = (float)json_number_value(threshold_3); - } + val = json_object_get(obj, "threshold_2"); + if (val && json_is_number(val)) + sc->threshold_2 = (float)json_number_value(val); - json_t *max_scale = json_object_get(cpu, "max_scale"); - if (max_scale && json_is_number(max_scale)) - { - config->temp_cpu_max_scale = (float)json_number_value(max_scale); - } + val = json_object_get(obj, "threshold_3"); + if (val && json_is_number(val)) + sc->threshold_3 = (float)json_number_value(val); - read_color_from_json(json_object_get(cpu, "threshold_1_color"), &config->temp_cpu_threshold_1_bar); - read_color_from_json(json_object_get(cpu, "threshold_2_color"), &config->temp_cpu_threshold_2_bar); - read_color_from_json(json_object_get(cpu, "threshold_3_color"), &config->temp_cpu_threshold_3_bar); - read_color_from_json(json_object_get(cpu, "threshold_4_color"), &config->temp_cpu_threshold_4_bar); -} + val = json_object_get(obj, "max_scale"); + if (val && json_is_number(val)) + sc->max_scale = (float)json_number_value(val); -/** - * @brief Load GPU temperature settings from JSON. - */ -static void load_gpu_temperature_from_json(json_t *root, Config *config) -{ - json_t *gpu = json_object_get(root, "gpu"); - if (!gpu || !json_is_object(gpu)) - return; + read_color_from_json(json_object_get(obj, "threshold_1_color"), &sc->threshold_1_bar); + read_color_from_json(json_object_get(obj, "threshold_2_color"), &sc->threshold_2_bar); + read_color_from_json(json_object_get(obj, "threshold_3_color"), &sc->threshold_3_bar); + read_color_from_json(json_object_get(obj, "threshold_4_color"), &sc->threshold_4_bar); - json_t *threshold_1 = json_object_get(gpu, "threshold_1"); - if (threshold_1 && json_is_number(threshold_1)) - { - config->temp_gpu_threshold_1 = (float)json_number_value(threshold_1); - } + val = json_object_get(obj, "offset_x"); + if (val && json_is_integer(val)) + sc->offset_x = (int)json_integer_value(val); - json_t *threshold_2 = json_object_get(gpu, "threshold_2"); - if (threshold_2 && json_is_number(threshold_2)) - { - config->temp_gpu_threshold_2 = (float)json_number_value(threshold_2); - } + val = json_object_get(obj, "offset_y"); + if (val && json_is_integer(val)) + sc->offset_y = (int)json_integer_value(val); - json_t *threshold_3 = json_object_get(gpu, "threshold_3"); - if (threshold_3 && json_is_number(threshold_3)) - { - config->temp_gpu_threshold_3 = (float)json_number_value(threshold_3); - } + val = json_object_get(obj, "font_size_temp"); + if (val && json_is_number(val)) + sc->font_size_temp = (float)json_number_value(val); - json_t *max_scale = json_object_get(gpu, "max_scale"); - if (max_scale && json_is_number(max_scale)) + val = json_object_get(obj, "label"); + if (val && json_is_string(val)) { - config->temp_gpu_max_scale = (float)json_number_value(max_scale); + const char *label_str = json_string_value(val); + if (label_str) + cc_safe_strcpy(sc->label, sizeof(sc->label), label_str); } - - read_color_from_json(json_object_get(gpu, "threshold_1_color"), &config->temp_gpu_threshold_1_bar); - read_color_from_json(json_object_get(gpu, "threshold_2_color"), &config->temp_gpu_threshold_2_bar); - read_color_from_json(json_object_get(gpu, "threshold_3_color"), &config->temp_gpu_threshold_3_bar); - read_color_from_json(json_object_get(gpu, "threshold_4_color"), &config->temp_gpu_threshold_4_bar); } /** - * @brief Load liquid temperature settings from JSON. + * @brief Load sensor configurations from JSON "sensors" map. + * @details New format: "sensors": { "cpu": {...}, "gpu": {...}, "uid:name": {...} } + * Also handles legacy format with top-level "cpu", "gpu", "liquid" objects. */ -static void load_liquid_from_json(json_t *root, Config *config) +static void load_sensors_from_json(json_t *root, Config *config) { - json_t *liquid = json_object_get(root, "liquid"); - if (!liquid || !json_is_object(liquid)) - return; - - json_t *max_scale = json_object_get(liquid, "max_scale"); - if (max_scale && json_is_number(max_scale)) + /* New format: "sensors" map */ + json_t *sensors = json_object_get(root, "sensors"); + if (sensors && json_is_object(sensors)) { - config->temp_liquid_max_scale = (float)json_number_value(max_scale); + const char *key; + json_t *value; + json_object_foreach(sensors, key, value) + { + if (!json_is_object(value)) + continue; + SensorConfig *sc = ensure_sensor_config(config, key); + if (sc) + load_sensor_config_from_json(value, sc); + } + return; /* New format takes priority */ } - json_t *threshold_1 = json_object_get(liquid, "threshold_1"); - if (threshold_1 && json_is_number(threshold_1)) + /* Legacy backward compatibility: top-level "cpu", "gpu", "liquid" */ + json_t *cpu = json_object_get(root, "cpu"); + if (cpu && json_is_object(cpu)) { - config->temp_liquid_threshold_1 = (float)json_number_value(threshold_1); + SensorConfig *sc = ensure_sensor_config(config, "cpu"); + if (sc) + load_sensor_config_from_json(cpu, sc); } - json_t *threshold_2 = json_object_get(liquid, "threshold_2"); - if (threshold_2 && json_is_number(threshold_2)) + json_t *gpu = json_object_get(root, "gpu"); + if (gpu && json_is_object(gpu)) { - config->temp_liquid_threshold_2 = (float)json_number_value(threshold_2); + SensorConfig *sc = ensure_sensor_config(config, "gpu"); + if (sc) + load_sensor_config_from_json(gpu, sc); } - json_t *threshold_3 = json_object_get(liquid, "threshold_3"); - if (threshold_3 && json_is_number(threshold_3)) + json_t *liquid = json_object_get(root, "liquid"); + if (liquid && json_is_object(liquid)) { - config->temp_liquid_threshold_3 = (float)json_number_value(threshold_3); + SensorConfig *sc = ensure_sensor_config(config, "liquid"); + if (sc) + load_sensor_config_from_json(liquid, sc); } - read_color_from_json(json_object_get(liquid, "threshold_1_color"), &config->temp_liquid_threshold_1_bar); - read_color_from_json(json_object_get(liquid, "threshold_2_color"), &config->temp_liquid_threshold_2_bar); - read_color_from_json(json_object_get(liquid, "threshold_3_color"), &config->temp_liquid_threshold_3_bar); - read_color_from_json(json_object_get(liquid, "threshold_4_color"), &config->temp_liquid_threshold_4_bar); + /* Legacy positioning offsets → migrate to SensorConfig */ + json_t *positioning = json_object_get(root, "positioning"); + if (positioning && json_is_object(positioning)) + { + const char *offset_keys[][3] = { + {"temp_offset_x_cpu", "temp_offset_y_cpu", "cpu"}, + {"temp_offset_x_gpu", "temp_offset_y_gpu", "gpu"}, + {"temp_offset_x_liquid", "temp_offset_y_liquid", "liquid"}}; + + for (int i = 0; i < 3; i++) + { + SensorConfig *sc = ensure_sensor_config(config, offset_keys[i][2]); + if (!sc) + continue; + + json_t *ox = json_object_get(positioning, offset_keys[i][0]); + if (ox && json_is_integer(ox)) + sc->offset_x = (int)json_integer_value(ox); + + json_t *oy = json_object_get(positioning, offset_keys[i][1]); + if (oy && json_is_integer(oy)) + sc->offset_y = (int)json_integer_value(oy); + } + } } /** - * @brief Load positioning settings from JSON. + * @brief Load global positioning settings from JSON. + * @details Per-sensor offsets are now handled in load_sensors_from_json(). */ static void load_positioning_from_json(json_t *root, Config *config) { @@ -962,42 +1105,6 @@ static void load_positioning_from_json(json_t *root, Config *config) if (!positioning || !json_is_object(positioning)) return; - json_t *temp_offset_x_cpu = json_object_get(positioning, "temp_offset_x_cpu"); - if (temp_offset_x_cpu && json_is_integer(temp_offset_x_cpu)) - { - config->display_temp_offset_x_cpu = (int)json_integer_value(temp_offset_x_cpu); - } - - json_t *temp_offset_x_gpu = json_object_get(positioning, "temp_offset_x_gpu"); - if (temp_offset_x_gpu && json_is_integer(temp_offset_x_gpu)) - { - config->display_temp_offset_x_gpu = (int)json_integer_value(temp_offset_x_gpu); - } - - json_t *temp_offset_y_cpu = json_object_get(positioning, "temp_offset_y_cpu"); - if (temp_offset_y_cpu && json_is_integer(temp_offset_y_cpu)) - { - config->display_temp_offset_y_cpu = (int)json_integer_value(temp_offset_y_cpu); - } - - json_t *temp_offset_y_gpu = json_object_get(positioning, "temp_offset_y_gpu"); - if (temp_offset_y_gpu && json_is_integer(temp_offset_y_gpu)) - { - config->display_temp_offset_y_gpu = (int)json_integer_value(temp_offset_y_gpu); - } - - json_t *temp_offset_x_liquid = json_object_get(positioning, "temp_offset_x_liquid"); - if (temp_offset_x_liquid && json_is_integer(temp_offset_x_liquid)) - { - config->display_temp_offset_x_liquid = (int)json_integer_value(temp_offset_x_liquid); - } - - json_t *temp_offset_y_liquid = json_object_get(positioning, "temp_offset_y_liquid"); - if (temp_offset_y_liquid && json_is_integer(temp_offset_y_liquid)) - { - config->display_temp_offset_y_liquid = (int)json_integer_value(temp_offset_y_liquid); - } - json_t *degree_spacing = json_object_get(positioning, "degree_spacing"); if (degree_spacing && json_is_integer(degree_spacing)) { @@ -1057,9 +1164,7 @@ int load_plugin_config(Config *config, const char *config_path) load_layout_from_json(root, config); load_colors_from_json(root, config); load_font_from_json(root, config); - load_cpu_temperature_from_json(root, config); - load_gpu_temperature_from_json(root, config); - load_liquid_from_json(root, config); + load_sensors_from_json(root, config); load_positioning_from_json(root, config); json_decref(root); diff --git a/src/device/config.h b/src/device/config.h index 494e8be..0f8e6b7 100644 --- a/src/device/config.h +++ b/src/device/config.h @@ -25,7 +25,7 @@ #define CONFIG_MAX_PASSWORD_LEN 128 #define CONFIG_MAX_PATH_LEN 512 #define CONFIG_MAX_FONT_NAME_LEN 64 -#define CONFIG_MAX_SENSOR_SLOT_LEN 32 +#define CONFIG_MAX_SENSOR_SLOT_LEN 256 /** * @brief Simple color structure. @@ -53,6 +53,36 @@ typedef enum LOG_ERROR } log_level_t; +// ============================================================================ +// Sensor Configuration (per-sensor thresholds, colors, offsets) +// ============================================================================ + +#define MAX_SENSOR_CONFIGS 32 +#define SENSOR_CONFIG_ID_LEN 256 + +/** + * @brief Per-sensor display configuration. + * @details Each configured sensor has its own thresholds, bar colors, and + * display offsets. Sensor ID format: legacy "cpu"/"gpu"/"liquid" or + * dynamic "device_uid:sensor_name". + */ +typedef struct +{ + char sensor_id[SENSOR_CONFIG_ID_LEN]; /**< Sensor identifier */ + float threshold_1; /**< Low threshold */ + float threshold_2; /**< Medium threshold */ + float threshold_3; /**< High threshold */ + float max_scale; /**< Maximum bar scale value */ + Color threshold_1_bar; /**< Bar color below threshold_1 */ + Color threshold_2_bar; /**< Bar color at threshold_1..2 */ + Color threshold_3_bar; /**< Bar color at threshold_2..3 */ + Color threshold_4_bar; /**< Bar color above threshold_3 */ + int offset_x; /**< Display X offset for value text */ + int offset_y; /**< Display Y offset for value text */ + float font_size_temp; /**< Per-sensor temp font size (0 = use global) */ + char label[32]; /**< Custom display label (empty = auto) */ +} SensorConfig; + /** * @brief Configuration structure. * @details Contains all settings for the CoolerDash system, including paths, @@ -109,46 +139,14 @@ typedef struct Config Color font_color_temp; Color font_color_label; - // Display positioning overrides - int display_temp_offset_x_cpu; - int display_temp_offset_x_gpu; - int display_temp_offset_y_cpu; - int display_temp_offset_y_gpu; - int display_temp_offset_x_liquid; - int display_temp_offset_y_liquid; + // Display positioning overrides (global) int display_degree_spacing; int display_label_offset_x; int display_label_offset_y; - // CPU temperature configuration - float temp_cpu_threshold_1; - float temp_cpu_threshold_2; - float temp_cpu_threshold_3; - float temp_cpu_max_scale; - Color temp_cpu_threshold_1_bar; - Color temp_cpu_threshold_2_bar; - Color temp_cpu_threshold_3_bar; - Color temp_cpu_threshold_4_bar; - - // GPU temperature configuration - float temp_gpu_threshold_1; - float temp_gpu_threshold_2; - float temp_gpu_threshold_3; - float temp_gpu_max_scale; - Color temp_gpu_threshold_1_bar; - Color temp_gpu_threshold_2_bar; - Color temp_gpu_threshold_3_bar; - Color temp_gpu_threshold_4_bar; - - // Liquid temperature configuration - float temp_liquid_threshold_1; - float temp_liquid_threshold_2; - float temp_liquid_threshold_3; - float temp_liquid_max_scale; - Color temp_liquid_threshold_1_bar; - Color temp_liquid_threshold_2_bar; - Color temp_liquid_threshold_3_bar; - Color temp_liquid_threshold_4_bar; + // Per-sensor configuration (thresholds, colors, offsets) + SensorConfig sensor_configs[MAX_SENSOR_CONFIGS]; /**< Sensor-specific configs */ + int sensor_config_count; /**< Number of valid entries */ } Config; /** @@ -208,4 +206,21 @@ static inline int is_valid_orientation(int orientation) */ int load_plugin_config(Config *config, const char *config_path); +/** + * @brief Find sensor configuration by sensor ID. + * @details Searches sensor_configs[] for a matching sensor_id. + * @param config Configuration struct + * @param sensor_id Sensor identifier ("cpu", "gpu", "liquid", or "uid:name") + * @return Pointer to matching SensorConfig, or NULL if not found + */ +const SensorConfig *get_sensor_config(const Config *config, const char *sensor_id); + +/** + * @brief Initialize a SensorConfig with default values for a given category. + * @param sc SensorConfig to initialize + * @param sensor_id Sensor identifier to set + * @param category 0=temp, 1=rpm, 2=duty, 3=watts, 4=freq + */ +void init_default_sensor_config(SensorConfig *sc, const char *sensor_id, int category); + #endif // CONFIG_H diff --git a/src/main.c b/src/main.c index 03dfa51..5e5de32 100644 --- a/src/main.c +++ b/src/main.c @@ -706,16 +706,17 @@ static void initialize_device_info(Config *config) log_message(LOG_STATUS, "Device: %s [%s]", name_display, uid_display); - if (get_temperature_monitor_data(config, &temp_data)) + if (get_sensor_monitor_data(config, &temp_data)) { - if (temp_data.temp_cpu > 0.0f || temp_data.temp_gpu > 0.0f) + if (temp_data.sensor_count > 0) { - log_message(LOG_STATUS, "Sensor values successfully detected"); + log_message(LOG_STATUS, "Sensor values successfully detected (%d sensors)", + temp_data.sensor_count); } else { log_message(LOG_WARNING, - "Sensor detection issues - temperature values not available"); + "Sensor detection issues - no sensor values available"); } } else diff --git a/src/mods/circle.c b/src/mods/circle.c index 2315770..b07a5fa 100644 --- a/src/mods/circle.c +++ b/src/mods/circle.c @@ -129,7 +129,7 @@ static void update_sensor_mode(const struct Config *config) if (verbose_logging) { const char *slot_value = get_slot_value_by_index(config, current_slot_index); - const char *label = get_slot_label(slot_value); + const char *label = get_slot_label(config, NULL, slot_value); log_message(LOG_INFO, "Circle mode: switched to %s display (slot: %s, interval: %.0fs)", label ? label : "unknown", @@ -161,7 +161,7 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config, // Get temperature and label for current slot const float temp_value = get_slot_temperature(data, slot_value); - const char *label_text = get_slot_label(slot_value); + const char *label_text = get_slot_label(config, data, slot_value); const float max_temp = get_slot_max_scale(config, slot_value); const int effective_bar_width = params->safe_bar_width; @@ -180,67 +180,61 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config, // Draw temperature value (centered horizontally INCLUDING degree symbol) char temp_str[16]; - // Use 1 decimal for liquid, integer for CPU/GPU - if (strcmp(slot_value, "liquid") == 0) + // Use decimal based on sensor type + if (get_slot_use_decimal(data, slot_value)) snprintf(temp_str, sizeof(temp_str), "%.1f", temp_value); else snprintf(temp_str, sizeof(temp_str), "%d", (int)temp_value); const Color *value_color = &config->font_color_temp; + // Use per-slot font size + float slot_font_size = get_slot_font_size(config, slot_value); + cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); - cairo_set_font_size(cr, config->font_size_temp); + cairo_set_font_size(cr, slot_font_size); set_cairo_color(cr, value_color); cairo_text_extents_t temp_ext; cairo_text_extents(cr, temp_str, &temp_ext); // Calculate degree symbol width for proper centering - cairo_set_font_size(cr, config->font_size_temp / 1.66); + cairo_set_font_size(cr, slot_font_size / 1.66); cairo_text_extents_t degree_ext; cairo_text_extents(cr, "°", °ree_ext); - cairo_set_font_size(cr, config->font_size_temp); + cairo_set_font_size(cr, slot_font_size); // Center temperature + degree symbol as a unit const double total_width = temp_ext.width - 4 + degree_ext.width; double temp_x = (config->display_width - total_width) / 2.0; double final_temp_y = temp_y; - // Apply user-defined offsets based on sensor type - if (strcmp(slot_value, "cpu") == 0) - { - if (config->display_temp_offset_x_cpu != -9999) - temp_x += config->display_temp_offset_x_cpu; - if (config->display_temp_offset_y_cpu != -9999) - final_temp_y += config->display_temp_offset_y_cpu; - } - else if (strcmp(slot_value, "gpu") == 0) - { - if (config->display_temp_offset_x_gpu != -9999) - temp_x += config->display_temp_offset_x_gpu; - if (config->display_temp_offset_y_gpu != -9999) - final_temp_y += config->display_temp_offset_y_gpu; - } - else if (strcmp(slot_value, "liquid") == 0) - { - if (config->display_temp_offset_x_liquid != -9999) - temp_x += config->display_temp_offset_x_liquid; - if (config->display_temp_offset_y_liquid != -9999) - final_temp_y += config->display_temp_offset_y_liquid; - } + // Apply user-defined offsets from sensor config + int offset_x = get_slot_offset_x(config, slot_value); + int offset_y = get_slot_offset_y(config, slot_value); + if (offset_x != 0) + temp_x += offset_x; + if (offset_y != 0) + final_temp_y += offset_y; cairo_move_to(cr, temp_x, final_temp_y); cairo_show_text(cr, temp_str); - // Draw degree symbol - const int degree_spacing = (config->display_degree_spacing > 0) - ? config->display_degree_spacing - : 16; - double degree_x = temp_x + temp_ext.width + degree_spacing; - double degree_y = final_temp_y - config->font_size_temp * 0.25; - - draw_degree_symbol(cr, degree_x, degree_y, config); + // Draw degree symbol only for temperature sensors + if (get_slot_is_temp(data, slot_value)) + { + const int degree_spacing = (config->display_degree_spacing > 0) + ? config->display_degree_spacing + : 16; + double degree_x = temp_x + temp_ext.width + degree_spacing; + double degree_y = final_temp_y - slot_font_size * 0.25; + + cairo_set_font_size(cr, slot_font_size / 1.66); + cairo_move_to(cr, degree_x, degree_y); + cairo_show_text(cr, "\xC2\xB0"); + cairo_set_font_size(cr, slot_font_size); + } // Draw temperature bar (centered reference point) @@ -350,9 +344,9 @@ static int render_circle_display(const struct Config *config, if (verbose_logging) { const char *slot_value = get_slot_value_by_index(config, current_slot_index); - const char *label = get_slot_label(slot_value); + const char *label = get_slot_label(config, data, slot_value); float temp = get_slot_temperature(data, slot_value); - log_message(LOG_INFO, "Circle mode: rendering %s (%.1f°C)", + log_message(LOG_INFO, "Circle mode: rendering %s (%.1f)", label ? label : "unknown", temp); } @@ -411,11 +405,11 @@ void draw_circle_image(const struct Config *config) get_liquidctl_data(config, device_uid, sizeof(device_uid), device_name, sizeof(device_name), &screen_width, &screen_height); - // Get temperature data + // Get sensor data monitor_sensor_data_t data = {0}; - if (!get_temperature_monitor_data(config, &data)) + if (!get_sensor_monitor_data(config, &data)) { - log_message(LOG_WARNING, "Circle mode: Failed to get temperature data"); + log_message(LOG_WARNING, "Circle mode: Failed to get sensor data"); return; } diff --git a/src/mods/display.c b/src/mods/display.c index 369239e..30959b5 100644 --- a/src/mods/display.c +++ b/src/mods/display.c @@ -254,90 +254,83 @@ int slot_is_active(const char *slot_value) } /** - * @brief Get temperature value for a sensor slot. + * @brief Get sensor value for a slot. */ float get_slot_temperature(const monitor_sensor_data_t *data, const char *slot_value) { if (!data || !slot_value) return 0.0f; - if (strcmp(slot_value, "cpu") == 0) - return data->temp_cpu; - else if (strcmp(slot_value, "gpu") == 0) - return data->temp_gpu; - else if (strcmp(slot_value, "liquid") == 0) - return data->temp_liquid; + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry) + return entry->value; return 0.0f; } /** * @brief Get display label for a sensor slot. + * @details Returns custom label from SensorConfig if set, otherwise + * uses legacy names or sensor name from data. */ -const char *get_slot_label(const char *slot_value) +const char *get_slot_label(const struct Config *config, + const monitor_sensor_data_t *data, + const char *slot_value) { - if (!slot_value || slot_value[0] == '\0') + if (!slot_value || slot_value[0] == '\0' || strcmp(slot_value, "none") == 0) return NULL; + /* Check for custom label override in SensorConfig */ + if (config) + { + const SensorConfig *sc = get_sensor_config(config, slot_value); + if (sc && sc->label[0] != '\0') + return sc->label; + } + + /* Legacy labels */ if (strcmp(slot_value, "cpu") == 0) return "CPU"; - else if (strcmp(slot_value, "gpu") == 0) + if (strcmp(slot_value, "gpu") == 0) return "GPU"; - else if (strcmp(slot_value, "liquid") == 0) + if (strcmp(slot_value, "liquid") == 0) return "LIQ"; - else if (strcmp(slot_value, "none") == 0) - return NULL; - return NULL; + /* Dynamic: use sensor name */ + if (data) + { + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry) + return entry->name; + } + + return "???"; } /** - * @brief Get bar color for a sensor slot based on temperature. + * @brief Get bar color for a sensor slot based on value. + * @details Uses SensorConfig thresholds via get_sensor_config(). */ -Color get_slot_bar_color(const struct Config *config, const char *slot_value, float temperature) +Color get_slot_bar_color(const struct Config *config, const char *slot_value, + float value) { + Color default_color = {0, 255, 0, 1}; + if (!config || !slot_value) - { - // Return default green color - Color default_color = {0, 255, 0, 1}; return default_color; - } - - // Liquid uses separate thresholds - if (strcmp(slot_value, "liquid") == 0) - { - if (temperature < config->temp_liquid_threshold_1) - return config->temp_liquid_threshold_1_bar; - else if (temperature < config->temp_liquid_threshold_2) - return config->temp_liquid_threshold_2_bar; - else if (temperature < config->temp_liquid_threshold_3) - return config->temp_liquid_threshold_3_bar; - else - return config->temp_liquid_threshold_4_bar; - } - // GPU uses separate thresholds - if (strcmp(slot_value, "gpu") == 0) - { - if (temperature < config->temp_gpu_threshold_1) - return config->temp_gpu_threshold_1_bar; - else if (temperature < config->temp_gpu_threshold_2) - return config->temp_gpu_threshold_2_bar; - else if (temperature < config->temp_gpu_threshold_3) - return config->temp_gpu_threshold_3_bar; - else - return config->temp_gpu_threshold_4_bar; - } + const SensorConfig *sc = get_sensor_config(config, slot_value); + if (!sc) + return default_color; - // CPU (default) uses separate thresholds - if (temperature < config->temp_cpu_threshold_1) - return config->temp_cpu_threshold_1_bar; - else if (temperature < config->temp_cpu_threshold_2) - return config->temp_cpu_threshold_2_bar; - else if (temperature < config->temp_cpu_threshold_3) - return config->temp_cpu_threshold_3_bar; + if (value < sc->threshold_1) + return sc->threshold_1_bar; + else if (value < sc->threshold_2) + return sc->threshold_2_bar; + else if (value < sc->threshold_3) + return sc->threshold_3_bar; else - return config->temp_cpu_threshold_4_bar; + return sc->threshold_4_bar; } /** @@ -348,16 +341,11 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value) if (!config) return 115.0f; - // Liquid has its own max scale (typically lower, e.g., 50°C) - if (slot_value && strcmp(slot_value, "liquid") == 0) - return config->temp_liquid_max_scale; + const SensorConfig *sc = get_sensor_config(config, slot_value); + if (sc && sc->max_scale > 0.0f) + return sc->max_scale; - // GPU has its own max scale - if (slot_value && strcmp(slot_value, "gpu") == 0) - return config->temp_gpu_max_scale; - - // CPU (default) max scale - return config->temp_cpu_max_scale; + return 115.0f; } /** @@ -366,7 +354,7 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value) uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name) { if (!config || !slot_name) - return 24; // Default fallback + return 24; if (strcmp(slot_name, "up") == 0) return config->layout_bar_height_up; @@ -375,7 +363,101 @@ uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name) else if (strcmp(slot_name, "down") == 0) return config->layout_bar_height_down; - return config->layout_bar_height; // Fallback to global + return config->layout_bar_height; +} + +/** + * @brief Get display unit string for a sensor slot. + */ +const char *get_slot_unit(const monitor_sensor_data_t *data, + const char *slot_value) +{ + if (!data || !slot_value) + return "\xC2\xB0" + "C"; + + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry) + return entry->unit; + + return "\xC2\xB0" + "C"; +} + +/** + * @brief Check if sensor should display decimal values. + */ +int get_slot_use_decimal(const monitor_sensor_data_t *data, + const char *slot_value) +{ + if (!data || !slot_value) + return 0; + + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry) + return entry->use_decimal; + + return 0; +} + +/** + * @brief Get display X offset for a sensor slot. + */ +int get_slot_offset_x(const struct Config *config, const char *slot_value) +{ + if (!config || !slot_value) + return 0; + + const SensorConfig *sc = get_sensor_config(config, slot_value); + return sc ? sc->offset_x : 0; +} + +/** + * @brief Get display Y offset for a sensor slot. + */ +int get_slot_offset_y(const struct Config *config, const char *slot_value) +{ + if (!config || !slot_value) + return 0; + + const SensorConfig *sc = get_sensor_config(config, slot_value); + return sc ? sc->offset_y : 0; +} + +/** + * @brief Check if a sensor slot is a temperature sensor. + */ +int get_slot_is_temp(const monitor_sensor_data_t *data, const char *slot_value) +{ + if (!data || !slot_value) + return 1; /* Default to temp */ + + /* Legacy slots are always temperature */ + if (strcmp(slot_value, "cpu") == 0 || strcmp(slot_value, "gpu") == 0 || + strcmp(slot_value, "liquid") == 0) + return 1; + + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry) + return (entry->category == SENSOR_CATEGORY_TEMP) ? 1 : 0; + + return 1; +} + +/** + * @brief Get font size for a sensor slot. + * @details Returns per-sensor font size if configured (> 0), otherwise global. + */ +float get_slot_font_size(const struct Config *config, const char *slot_value) +{ + if (!config) + return 100.0f; + + const SensorConfig *sc = get_sensor_config(config, slot_value); + if (sc && sc->font_size_temp > 0.0f) + return sc->font_size_temp; + + return config->font_size_temp; } /** diff --git a/src/mods/display.h b/src/mods/display.h index b8c1d46..09cf6ab 100644 --- a/src/mods/display.h +++ b/src/mods/display.h @@ -138,34 +138,39 @@ void draw_degree_symbol(cairo_t *cr, double x, double y, int slot_is_active(const char *slot_value); /** - * @brief Get temperature value for a sensor slot. - * @param data Sensor data structure with CPU, GPU, and liquid temperatures - * @param slot_value Slot configuration value ("cpu", "gpu", "liquid") - * @return Temperature in Celsius, or 0.0 if slot is "none" or invalid + * @brief Get sensor value for a slot. + * @param data Sensor data collection + * @param slot_value Slot configuration value ("cpu","gpu","liquid" or "uid:name") + * @return Sensor value, or 0.0 if slot is "none" or sensor not found */ float get_slot_temperature(const monitor_sensor_data_t *data, const char *slot_value); /** * @brief Get display label for a sensor slot. - * @param slot_value Slot configuration value ("cpu", "gpu", "liquid", "none") - * @return Label string ("CPU", "GPU", "LIQ") or NULL if "none" + * @param config Configuration with sensor configs (for custom label override) + * @param data Sensor data collection (used for dynamic sensor names) + * @param slot_value Slot configuration value + * @return Label string (custom, "CPU","GPU","LIQ" or sensor name) or NULL */ -const char *get_slot_label(const char *slot_value); +const char *get_slot_label(const struct Config *config, + const monitor_sensor_data_t *data, + const char *slot_value); /** - * @brief Get bar color for a sensor slot based on temperature. - * @param config Configuration with threshold colors - * @param slot_value Slot configuration value (determines which thresholds to use) - * @param temperature Current temperature value - * @return Color based on temperature thresholds + * @brief Get bar color for a sensor slot based on value. + * @param config Configuration with sensor threshold configs + * @param slot_value Slot configuration value + * @param value Current sensor value + * @return Color based on thresholds */ -Color get_slot_bar_color(const struct Config *config, const char *slot_value, float temperature); +Color get_slot_bar_color(const struct Config *config, const char *slot_value, + float value); /** * @brief Get maximum scale for a sensor slot. - * @param config Configuration with max scale values + * @param config Configuration with sensor configs * @param slot_value Slot configuration value - * @return Maximum temperature scale (liquid uses different max) + * @return Maximum scale value */ float get_slot_max_scale(const struct Config *config, const char *slot_value); @@ -177,4 +182,54 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value); */ uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name); +/** + * @brief Get display unit string for a sensor slot. + * @param data Sensor data collection + * @param slot_value Slot configuration value + * @return Unit string ("°C","RPM","%","W","MHz") or "°C" as default + */ +const char *get_slot_unit(const monitor_sensor_data_t *data, + const char *slot_value); + +/** + * @brief Check if sensor should display decimal values. + * @param data Sensor data collection + * @param slot_value Slot configuration value + * @return 1 for decimal display, 0 for integer + */ +int get_slot_use_decimal(const monitor_sensor_data_t *data, + const char *slot_value); + +/** + * @brief Get display X offset for a sensor slot. + * @param config Configuration with sensor configs + * @param slot_value Slot configuration value + * @return X offset in pixels + */ +int get_slot_offset_x(const struct Config *config, const char *slot_value); + +/** + * @brief Get display Y offset for a sensor slot. + * @param config Configuration with sensor configs + * @param slot_value Slot configuration value + * @return Y offset in pixels + */ +int get_slot_offset_y(const struct Config *config, const char *slot_value); + +/** + * @brief Check if a sensor slot is a temperature sensor. + * @param data Sensor data collection + * @param slot_value Slot configuration value + * @return 1 if temperature sensor (show degree symbol), 0 otherwise + */ +int get_slot_is_temp(const monitor_sensor_data_t *data, const char *slot_value); + +/** + * @brief Get font size for a sensor slot. + * @param config Configuration with sensor configs and global font size + * @param slot_value Slot configuration value + * @return Per-sensor font size if set, otherwise global font_size_temp + */ +float get_slot_font_size(const struct Config *config, const char *slot_value); + #endif // DISPLAY_DISPATCHER_H diff --git a/src/mods/dual.c b/src/mods/dual.c index 198a9c3..d3ad658 100644 --- a/src/mods/dual.c +++ b/src/mods/dual.c @@ -100,77 +100,114 @@ static void draw_temperature_displays(cairo_t *cr, if (!up_active && down_active) down_bar_y = start_y; - cairo_font_extents_t font_ext; - cairo_font_extents(cr, &font_ext); - - // Calculate reference width (widest 2-digit number) for sub-100 alignment - cairo_text_extents_t ref_width_ext; - cairo_text_extents(cr, "88", &ref_width_ext); - // Draw upper slot temperature if (up_active) { + // Set per-slot font size + float up_font_size = get_slot_font_size(config, slot_up); + cairo_set_font_size(cr, up_font_size); + + cairo_font_extents_t up_font_ext; + cairo_font_extents(cr, &up_font_ext); + + // Calculate reference width (widest 2-digit number) for sub-100 alignment + cairo_text_extents_t up_ref_ext; + cairo_text_extents(cr, "88", &up_ref_ext); + char up_num_str[16]; - snprintf(up_num_str, sizeof(up_num_str), "%d", (int)temp_up); + if (get_slot_use_decimal(data, slot_up)) + snprintf(up_num_str, sizeof(up_num_str), "%.1f", temp_up); + else + snprintf(up_num_str, sizeof(up_num_str), "%d", (int)temp_up); cairo_text_extents_t up_num_ext; cairo_text_extents(cr, up_num_str, &up_num_ext); - double up_width = (temp_up >= 100.0f) ? up_num_ext.width : ref_width_ext.width; + double up_width = (temp_up >= 100.0f) ? up_num_ext.width : up_ref_ext.width; double up_temp_x = bar_x + (effective_bar_width - up_width) / 2.0; if (config->display_width == 240 && config->display_height == 240) up_temp_x += 20; - if (config->display_temp_offset_x_cpu != 0) - up_temp_x += config->display_temp_offset_x_cpu; + int offset_x_up = get_slot_offset_x(config, slot_up); + if (offset_x_up != 0) + up_temp_x += offset_x_up; - double up_temp_y = up_bar_y + 8 - font_ext.descent; - if (config->display_temp_offset_y_cpu != 0) - up_temp_y += config->display_temp_offset_y_cpu; + double up_temp_y = up_bar_y + 8 - up_font_ext.descent; + int offset_y_up = get_slot_offset_y(config, slot_up); + if (offset_y_up != 0) + up_temp_y += offset_y_up; cairo_move_to(cr, up_temp_x, up_temp_y); cairo_show_text(cr, up_num_str); - // Degree symbol - const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16; - double degree_up_x = up_temp_x + up_width + degree_spacing; - double degree_up_y = up_temp_y - up_num_ext.height * 0.40; - draw_degree_symbol(cr, degree_up_x, degree_up_y, config); + // Draw degree symbol or unit + if (get_slot_is_temp(data, slot_up)) + { + const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16; + double degree_up_x = up_temp_x + up_width + degree_spacing; + double degree_up_y = up_temp_y - up_num_ext.height * 0.40; + cairo_set_font_size(cr, up_font_size / 1.66); + cairo_move_to(cr, degree_up_x, degree_up_y); + cairo_show_text(cr, "\xC2\xB0"); + cairo_set_font_size(cr, up_font_size); + } } // Draw lower slot temperature if (down_active) { + // Set per-slot font size + float down_font_size = get_slot_font_size(config, slot_down); + cairo_set_font_size(cr, down_font_size); + + cairo_font_extents_t down_font_ext; + cairo_font_extents(cr, &down_font_ext); + + // Calculate reference width for sub-100 alignment + cairo_text_extents_t down_ref_ext; + cairo_text_extents(cr, "88", &down_ref_ext); + char down_num_str[16]; - snprintf(down_num_str, sizeof(down_num_str), "%d", (int)temp_down); + if (get_slot_use_decimal(data, slot_down)) + snprintf(down_num_str, sizeof(down_num_str), "%.1f", temp_down); + else + snprintf(down_num_str, sizeof(down_num_str), "%d", (int)temp_down); cairo_text_extents_t down_num_ext; cairo_text_extents(cr, down_num_str, &down_num_ext); - double down_width = (temp_down >= 100.0f) ? down_num_ext.width : ref_width_ext.width; + double down_width = (temp_down >= 100.0f) ? down_num_ext.width : down_ref_ext.width; double down_temp_x = bar_x + (effective_bar_width - down_width) / 2.0; if (config->display_width == 240 && config->display_height == 240) down_temp_x += 20; - if (config->display_temp_offset_x_gpu != 0) - down_temp_x += config->display_temp_offset_x_gpu; + int offset_x_down = get_slot_offset_x(config, slot_down); + if (offset_x_down != 0) + down_temp_x += offset_x_down; // Use the actual bar height for positioning uint16_t effective_down_height = down_active ? bar_height_down : 0; - double down_temp_y = down_bar_y + effective_down_height - 4 + font_ext.ascent; - if (config->display_temp_offset_y_gpu != 0) - down_temp_y += config->display_temp_offset_y_gpu; + double down_temp_y = down_bar_y + effective_down_height - 4 + down_font_ext.ascent; + int offset_y_down = get_slot_offset_y(config, slot_down); + if (offset_y_down != 0) + down_temp_y += offset_y_down; cairo_move_to(cr, down_temp_x, down_temp_y); cairo_show_text(cr, down_num_str); - // Degree symbol - const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16; - double degree_down_x = down_temp_x + down_width + degree_spacing; - double degree_down_y = down_temp_y - down_num_ext.height * 0.40; - draw_degree_symbol(cr, degree_down_x, degree_down_y, config); + // Draw degree symbol or unit + if (get_slot_is_temp(data, slot_down)) + { + const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16; + double degree_down_x = down_temp_x + down_width + degree_spacing; + double degree_down_y = down_temp_y - down_num_ext.height * 0.40; + cairo_set_font_size(cr, down_font_size / 1.66); + cairo_move_to(cr, degree_down_x, degree_down_y); + cairo_show_text(cr, "\xC2\xB0"); + cairo_set_font_size(cr, down_font_size); + } } } @@ -294,7 +331,7 @@ static void draw_labels(cairo_t *cr, const struct Config *config, const monitor_sensor_data_t *data, const ScalingParams *params) { - (void)data; // Reserved for future use (e.g., dynamic labels based on values) + (void)data; if (!cr || !config || !params) return; @@ -306,8 +343,8 @@ static void draw_labels(cairo_t *cr, const struct Config *config, const int down_active = slot_is_active(slot_down); // Get labels from slots (NULL if "none") - const char *label_up = get_slot_label(slot_up); - const char *label_down = get_slot_label(slot_down); + const char *label_up = get_slot_label(config, data, slot_up); + const char *label_down = get_slot_label(config, data, slot_down); // Get bar heights const uint16_t bar_height_up = get_slot_bar_height(config, "up"); @@ -401,8 +438,17 @@ static void render_display_content(cairo_t *cr, const struct Config *config, float temp_up = get_slot_temperature(data, config->sensor_slot_up); float temp_down = get_slot_temperature(data, config->sensor_slot_down); - // Labels only if both temps < 99°C (to avoid overlap with large numbers) - if (temp_up < 99.0f && temp_down < 99.0f) + // Labels only if temperature sensors < 99°C (to avoid overlap with large numbers) + // Non-temperature sensors always show labels (RPM, Watts etc. have different scales) + int up_is_temp = get_slot_is_temp(data, config->sensor_slot_up); + int down_is_temp = get_slot_is_temp(data, config->sensor_slot_down); + int show_labels = 1; + if (up_is_temp && temp_up >= 99.0f) + show_labels = 0; + if (down_is_temp && temp_down >= 99.0f) + show_labels = 0; + + if (show_labels) { cairo_set_font_size(cr, config->font_size_labels); set_cairo_color(cr, &config->font_color_label); @@ -488,10 +534,10 @@ void draw_dual_image(const struct Config *config) } // Get sensor data - monitor_sensor_data_t sensor_data = {.temp_cpu = 0.0f, .temp_gpu = 0.0f}; - if (!get_temperature_monitor_data(config, &sensor_data)) + monitor_sensor_data_t sensor_data = {0}; + if (!get_sensor_monitor_data(config, &sensor_data)) { - log_message(LOG_WARNING, "Failed to retrieve temperature data"); + log_message(LOG_WARNING, "Failed to retrieve sensor data"); return; } diff --git a/src/srv/cc_conf.c b/src/srv/cc_conf.c index 5e6cff8..7ff25ec 100644 --- a/src/srv/cc_conf.c +++ b/src/srv/cc_conf.c @@ -69,10 +69,52 @@ static struct int is_circular; } device_cache = {0}; +/** + * @brief Cache for all device names and types (populated from /devices). + * @details Maps device UID to display name and type string. + */ +static struct +{ + char uid[128]; + char name[CC_NAME_SIZE]; + char type[16]; +} device_name_cache[MAX_DEVICE_NAME_CACHE]; +static int device_name_cache_count = 0; + +/** + * @brief Get device display name by UID. + */ +const char *get_device_name_by_uid(const char *device_uid) +{ + if (!device_uid) + return ""; + for (int i = 0; i < device_name_cache_count; i++) + { + if (strcmp(device_name_cache[i].uid, device_uid) == 0) + return device_name_cache[i].name; + } + return ""; +} + +/** + * @brief Get device type string by UID. + */ +const char *get_device_type_by_uid(const char *device_uid) +{ + if (!device_uid) + return NULL; + for (int i = 0; i < device_name_cache_count; i++) + { + if (strcmp(device_name_cache[i].uid, device_uid) == 0) + return device_name_cache[i].type; + } + return NULL; +} + /** * @brief Extract device type from JSON device object. * @details Common helper function to extract device type string from JSON - * device object. + * device object. Checks "type" first (/devices), falls back to "d_type" (/status). */ const char *extract_device_type_from_json(const json_t *dev) { @@ -81,7 +123,12 @@ const char *extract_device_type_from_json(const json_t *dev) const json_t *type_val = json_object_get(dev, "type"); if (!type_val || !json_is_string(type_val)) - return NULL; + { + /* Fallback: /status endpoint uses "d_type" instead of "type" */ + type_val = json_object_get(dev, "d_type"); + if (!type_val || !json_is_string(type_val)) + return NULL; + } return json_string_value(type_val); } @@ -356,11 +403,77 @@ static void configure_device_cache_curl(CURL *curl, const char *url, curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers); } +/** + * @brief Populate device name cache from parsed JSON devices array. + * @details Caches UID, name, and type for all devices (not just Liquidctl). + */ +static void populate_device_name_cache(const char *json_data) +{ + if (!json_data) + return; + + json_error_t error; + json_t *root = json_loads(json_data, 0, &error); + if (!root) + return; + + const json_t *devices = json_object_get(root, "devices"); + if (!devices || !json_is_array(devices)) + { + json_decref(root); + return; + } + + device_name_cache_count = 0; + const size_t count = json_array_size(devices); + for (size_t i = 0; i < count && device_name_cache_count < MAX_DEVICE_NAME_CACHE; i++) + { + const json_t *dev = json_array_get(devices, i); + if (!dev) + continue; + + const json_t *uid_val = json_object_get(dev, "uid"); + const json_t *name_val = json_object_get(dev, "name"); + const char *type_str = extract_device_type_from_json(dev); + + if (!uid_val || !json_is_string(uid_val)) + continue; + + int idx = device_name_cache_count; + cc_safe_strcpy(device_name_cache[idx].uid, + sizeof(device_name_cache[idx].uid), + json_string_value(uid_val)); + + if (name_val && json_is_string(name_val)) + cc_safe_strcpy(device_name_cache[idx].name, + sizeof(device_name_cache[idx].name), + json_string_value(name_val)); + else + device_name_cache[idx].name[0] = '\0'; + + if (type_str) + cc_safe_strcpy(device_name_cache[idx].type, + sizeof(device_name_cache[idx].type), + type_str); + else + device_name_cache[idx].type[0] = '\0'; + + device_name_cache_count++; + } + + log_message(LOG_INFO, "Device name cache: %d devices cached", + device_name_cache_count); + json_decref(root); +} + /** * @brief Process device cache API response and populate cache. */ static int process_device_cache_response(const http_response *chunk) { + /* Populate device name cache for ALL devices (used by sensor system) */ + populate_device_name_cache(chunk->data); + int found_liquidctl = 0; int result = parse_liquidctl_data( chunk->data, device_cache.device_uid, sizeof(device_cache.device_uid), diff --git a/src/srv/cc_conf.h b/src/srv/cc_conf.h index 37ad27f..678621c 100644 --- a/src/srv/cc_conf.h +++ b/src/srv/cc_conf.h @@ -18,6 +18,7 @@ // Include necessary headers // cppcheck-suppress-begin missingIncludeSystem #include +#include // cppcheck-suppress-end missingIncludeSystem // Include project headers @@ -25,6 +26,7 @@ // Basic constants #define CC_NAME_SIZE 128 +#define MAX_DEVICE_NAME_CACHE 32 // Forward declarations struct Config; @@ -71,4 +73,29 @@ int update_config_from_device(struct Config *config); int is_circular_display_device(const char *device_name, int screen_width, int screen_height); +/** + * @brief Get device display name by UID. + * @details Retrieves the cached device name for a given UID. + * Cache is populated during init_device_cache(). + * @param device_uid Device UID to look up + * @return Device name string, or empty string if not found + */ +const char *get_device_name_by_uid(const char *device_uid); + +/** + * @brief Get device type string by UID. + * @param device_uid Device UID to look up + * @return Device type string ("CPU","GPU","Liquidctl","Hwmon","CustomSensors"), + * or NULL if not found + */ +const char *get_device_type_by_uid(const char *device_uid); + +/** + * @brief Extract device type from JSON device object. + * @details Checks "type" field first, falls back to "d_type" (used in /status). + * @param dev JSON device object + * @return Device type string, or NULL if not found + */ +const char *extract_device_type_from_json(const json_t *dev); + #endif // CC_CONF_H diff --git a/src/srv/cc_sensor.c b/src/srv/cc_sensor.c index 8739db9..e9f9603 100644 --- a/src/srv/cc_sensor.c +++ b/src/srv/cc_sensor.c @@ -8,8 +8,9 @@ */ /** - * @brief CPU/GPU temperature monitoring via CoolerControl API. - * @details Reads sensor data from /status endpoint. + * @brief Sensor monitoring via CoolerControl API. + * @details Reads all sensor data (temps, RPM, duty, watts, freq) from + * /status endpoint for all device types. */ // Include necessary headers @@ -27,12 +28,6 @@ #include "cc_main.h" #include "cc_sensor.h" -/** - * @brief Extract device type from JSON object. - * @details Helper function to extract device type from JSON object. - */ -extern const char *extract_device_type_from_json(const json_t *dev); - /** @brief Cached CURL handle for reuse across polling cycles. */ static CURL *sensor_curl_handle = NULL; @@ -65,32 +60,60 @@ void cleanup_sensor_curl_handle(void) } /** - * @brief Extract temperature from device status history. - * @details Helper function to get temperature from the latest status entry. + * @brief Add a sensor entry to the monitor data collection. + * @details Helper to append a new sensor entry with bounds checking. + * @return 1 if added, 0 if array full */ -static float extract_device_temperature(const json_t *device, - const char *device_type) +static int add_sensor_entry(monitor_sensor_data_t *data, + const char *name, const char *device_uid, + const char *device_type, sensor_category_t category, + float value, const char *unit, int use_decimal) +{ + if (data->sensor_count >= MAX_SENSORS) + return 0; + + sensor_entry_t *entry = &data->sensors[data->sensor_count]; + cc_safe_strcpy(entry->name, sizeof(entry->name), name); + cc_safe_strcpy(entry->device_uid, sizeof(entry->device_uid), device_uid); + cc_safe_strcpy(entry->device_type, sizeof(entry->device_type), device_type); + cc_safe_strcpy(entry->unit, sizeof(entry->unit), unit); + + /* Device name from cache */ + const char *dev_name = get_device_name_by_uid(device_uid); + cc_safe_strcpy(entry->device_name, sizeof(entry->device_name), dev_name); + + entry->category = category; + entry->value = value; + entry->use_decimal = use_decimal; + + data->sensor_count++; + return 1; +} + +/** + * @brief Collect all temperature sensors from a device's latest status. + * @details Iterates temps[] array in the last status_history entry. + */ +static void collect_device_temps(const json_t *device, const char *device_uid, + const char *device_type, + monitor_sensor_data_t *data) { - // Get status history const json_t *status_history = json_object_get(device, "status_history"); if (!status_history || !json_is_array(status_history)) - return 0.0f; + return; size_t history_count = json_array_size(status_history); if (history_count == 0) - return 0.0f; + return; - // Get latest status const json_t *last_status = json_array_get(status_history, history_count - 1); if (!last_status) - return 0.0f; + return; - // Get temperatures array const json_t *temps = json_object_get(last_status, "temps"); if (!temps || !json_is_array(temps)) - return 0.0f; + return; - // Search for appropriate temperature sensor size_t temp_count = json_array_size(temps); for (size_t i = 0; i < temp_count; i++) { @@ -101,65 +124,139 @@ static float extract_device_temperature(const json_t *device, const json_t *name_val = json_object_get(temp_entry, "name"); const json_t *temp_val = json_object_get(temp_entry, "temp"); - if (!name_val || !json_is_string(name_val) || !temp_val || - !json_is_number(temp_val)) + if (!name_val || !json_is_string(name_val) || + !temp_val || !json_is_number(temp_val)) continue; - const char *sensor_name = json_string_value(name_val); float temperature = (float)json_number_value(temp_val); - // Validate temperature range - if (temperature < -50.0f || temperature > 150.0f) + /* Skip invalid readings */ + if (temperature < -50.0f || temperature > 200.0f) continue; - // Check sensor name based on device type - if (strcmp(device_type, "CPU") == 0 && strcmp(sensor_name, "temp1") == 0) + /* Decimal only for Liquidctl (coolant) sensors */ + int use_dec = (strcmp(device_type, "Liquidctl") == 0) ? 1 : 0; + + add_sensor_entry(data, json_string_value(name_val), device_uid, + device_type, SENSOR_CATEGORY_TEMP, temperature, + "\xC2\xB0" + "C", + use_dec); + } +} + +/** + * @brief Collect all channel sensors from a device's latest status. + * @details Iterates channels[] array and creates entries for RPM, duty, + * watts, and freq values. + */ +static void collect_device_channels(const json_t *device, + const char *device_uid, + const char *device_type, + monitor_sensor_data_t *data) +{ + const json_t *status_history = json_object_get(device, "status_history"); + if (!status_history || !json_is_array(status_history)) + return; + + size_t history_count = json_array_size(status_history); + if (history_count == 0) + return; + + const json_t *last_status = json_array_get(status_history, history_count - 1); + if (!last_status) + return; + + const json_t *channels = json_object_get(last_status, "channels"); + if (!channels || !json_is_array(channels)) + return; + + size_t channel_count = json_array_size(channels); + for (size_t i = 0; i < channel_count; i++) + { + const json_t *ch = json_array_get(channels, i); + if (!ch) + continue; + + const json_t *name_val = json_object_get(ch, "name"); + if (!name_val || !json_is_string(name_val)) + continue; + + const char *ch_name = json_string_value(name_val); + char sensor_name[SENSOR_NAME_LEN]; + + /* RPM */ + const json_t *rpm_val = json_object_get(ch, "rpm"); + if (rpm_val && json_is_number(rpm_val)) { - return temperature; + float rpm = (float)json_number_value(rpm_val); + if (rpm >= 0.0f) + { + int n = snprintf(sensor_name, sizeof(sensor_name), + "%s RPM", ch_name); + if (n > 0 && (size_t)n < sizeof(sensor_name)) + add_sensor_entry(data, sensor_name, device_uid, + device_type, SENSOR_CATEGORY_RPM, + rpm, "RPM", 0); + } } - else if (strcmp(device_type, "GPU") == 0 && - (strstr(sensor_name, "GPU") || strstr(sensor_name, "gpu") || - strstr(sensor_name, "temp1"))) + + /* Duty cycle */ + const json_t *duty_val = json_object_get(ch, "duty"); + if (duty_val && json_is_number(duty_val)) { - return temperature; + float duty = (float)json_number_value(duty_val); + int n = snprintf(sensor_name, sizeof(sensor_name), + "%s Duty", ch_name); + if (n > 0 && (size_t)n < sizeof(sensor_name)) + add_sensor_entry(data, sensor_name, device_uid, + device_type, SENSOR_CATEGORY_DUTY, + duty, "%", 1); } - else if (strcmp(device_type, "Liquidctl") == 0 && - (strstr(sensor_name, "Liquid") || - strstr(sensor_name, "liquid") || - strstr(sensor_name, "Coolant") || - strstr(sensor_name, "coolant"))) + + /* Watts */ + const json_t *watts_val = json_object_get(ch, "watts"); + if (watts_val && json_is_number(watts_val)) { - return temperature; + float watts = (float)json_number_value(watts_val); + int n = snprintf(sensor_name, sizeof(sensor_name), + "%s Power", ch_name); + if (n > 0 && (size_t)n < sizeof(sensor_name)) + add_sensor_entry(data, sensor_name, device_uid, + device_type, SENSOR_CATEGORY_WATTS, + watts, "W", 1); } - } - return 0.0f; + /* Frequency */ + const json_t *freq_val = json_object_get(ch, "freq"); + if (freq_val && json_is_number(freq_val)) + { + float freq = (float)json_number_value(freq_val); + int n = snprintf(sensor_name, sizeof(sensor_name), + "%s Freq", ch_name); + if (n > 0 && (size_t)n < sizeof(sensor_name)) + add_sensor_entry(data, sensor_name, device_uid, + device_type, SENSOR_CATEGORY_FREQ, + freq, "MHz", 0); + } + } } /** - * @brief Parse sensor JSON and extract temperatures from CPU, GPU, and - * Liquidctl devices. - * @details Simplified JSON parsing to extract CPU, GPU, and Liquid temperature - * values. + * @brief Parse /status JSON and collect all sensors from all devices. + * @details Iterates all devices and collects temperature and channel data + * into the monitor_sensor_data_t structure. */ -static int parse_temperature_data(const char *json, float *temp_cpu, - float *temp_gpu, float *temp_liquid) +static int parse_all_sensor_data(const char *json, monitor_sensor_data_t *data) { - if (!json || json[0] == '\0') + if (!json || json[0] == '\0' || !data) { - log_message(LOG_ERROR, "Invalid JSON input"); + log_message(LOG_ERROR, "Invalid input for sensor parsing"); return 0; } - // Initialize outputs - if (temp_cpu) - *temp_cpu = 0.0f; - if (temp_gpu) - *temp_gpu = 0.0f; - if (temp_liquid) - *temp_liquid = 0.0f; + data->sensor_count = 0; - // Parse JSON json_error_t json_error; json_t *root = json_loads(json, 0, &json_error); if (!root) @@ -168,7 +265,6 @@ static int parse_temperature_data(const char *json, float *temp_cpu, return 0; } - // Get devices array const json_t *devices = json_object_get(root, "devices"); if (!devices || !json_is_array(devices)) { @@ -176,12 +272,8 @@ static int parse_temperature_data(const char *json, float *temp_cpu, return 0; } - // Search for CPU, GPU, and Liquidctl devices size_t device_count = json_array_size(devices); - int cpu_found = 0, gpu_found = 0, liquid_found = 0; - - for (size_t i = 0; - i < device_count && (!cpu_found || !gpu_found || !liquid_found); i++) + for (size_t i = 0; i < device_count; i++) { const json_t *device = json_array_get(devices, i); if (!device) @@ -191,36 +283,21 @@ static int parse_temperature_data(const char *json, float *temp_cpu, if (!device_type) continue; - if (!cpu_found && strcmp(device_type, "CPU") == 0) - { - if (temp_cpu) - { - *temp_cpu = extract_device_temperature(device, "CPU"); - cpu_found = 1; - } - } - else if (!gpu_found && strcmp(device_type, "GPU") == 0) - { - if (temp_gpu) - { - *temp_gpu = extract_device_temperature(device, "GPU"); - gpu_found = 1; - } - } - else if (!liquid_found && strcmp(device_type, "Liquidctl") == 0) - { - if (temp_liquid) - { - *temp_liquid = extract_device_temperature(device, "Liquidctl"); - if (*temp_liquid > 0.0f) - { - liquid_found = 1; - } - } - } + /* Extract device UID */ + const json_t *uid_val = json_object_get(device, "uid"); + if (!uid_val || !json_is_string(uid_val)) + continue; + + const char *device_uid = json_string_value(uid_val); + + /* Collect all temps and channels from this device */ + collect_device_temps(device, device_uid, device_type, data); + collect_device_channels(device, device_uid, device_type, data); } json_decref(root); + log_message(LOG_INFO, "Parsed %d sensors from %zu devices", + data->sensor_count, device_count); return 1; } @@ -246,20 +323,16 @@ static void configure_status_request(CURL *curl, const char *url, } /** - * @brief Get CPU, GPU, and Liquid temperature data from CoolerControl API. - * @details Simplified HTTP request to get temperature data from status - * endpoint. + * @brief Get all sensor data from CoolerControl /status API. + * @details Polls the API and collects all sensors into the data structure. */ -static int get_temperature_data(const Config *config, float *temp_cpu, - float *temp_gpu, float *temp_liquid) +static int get_sensor_data_from_api(const Config *config, + monitor_sensor_data_t *data) { - if (!config || !temp_cpu || !temp_gpu || !temp_liquid) + if (!config || !data) return 0; - // Initialize outputs - *temp_cpu = 0.0f; - *temp_gpu = 0.0f; - *temp_liquid = 0.0f; + data->sensor_count = 0; if (config->daemon_address[0] == '\0') { @@ -267,21 +340,17 @@ static int get_temperature_data(const Config *config, float *temp_cpu, return 0; } - // Get cached CURL handle for sensor polling CURL *curl = get_sensor_curl_handle(); if (!curl) return 0; - // Reset handle state for clean request curl_easy_reset(curl); - // Build URL char url[256]; int url_len = snprintf(url, sizeof(url), "%s/status", config->daemon_address); if (url_len < 0 || url_len >= (int)sizeof(url)) return 0; - // Initialize response buffer struct http_response response = {0}; if (!cc_init_response_buffer(&response, 8192)) { @@ -289,23 +358,19 @@ static int get_temperature_data(const Config *config, float *temp_cpu, return 0; } - // Configure request configure_status_request(curl, url, &response); - // Enable SSL verification for HTTPS if (strncmp(config->daemon_address, "https://", 8) == 0) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); } - // Set headers struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "accept: application/json"); headers = curl_slist_append(headers, "content-type: application/json"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - // Perform request and parse response int result = 0; CURLcode curl_result = curl_easy_perform(curl); if (curl_result == CURLE_OK) @@ -315,12 +380,11 @@ static int get_temperature_data(const Config *config, float *temp_cpu, if (response_code == 200) { - result = parse_temperature_data(response.data, temp_cpu, temp_gpu, - temp_liquid); + result = parse_all_sensor_data(response.data, data); } else { - log_message(LOG_ERROR, "HTTP error: %ld when fetching temperature data", + log_message(LOG_ERROR, "HTTP error: %ld when fetching sensor data", response_code); } } @@ -329,7 +393,6 @@ static int get_temperature_data(const Config *config, float *temp_cpu, log_message(LOG_ERROR, "CURL error: %s", curl_easy_strerror(curl_result)); } - // Cleanup request-specific resources cc_cleanup_response_buffer(&response); if (headers) curl_slist_free_all(headers); @@ -337,18 +400,104 @@ static int get_temperature_data(const Config *config, float *temp_cpu, return result; } +// ============================================================================ +// Slot Resolution Functions +// ============================================================================ + /** - * @brief Get all relevant sensor data (CPU/GPU/Liquid temperature and LCD UID). - * @details Reads the current CPU, GPU, and Liquid temperatures via API. + * @brief Check if a slot value is a legacy type. */ -int get_temperature_monitor_data(const Config *config, - monitor_sensor_data_t *data) +int is_legacy_sensor_slot(const char *slot_value) +{ + if (!slot_value) + return 0; + return (strcmp(slot_value, "cpu") == 0 || + strcmp(slot_value, "gpu") == 0 || + strcmp(slot_value, "liquid") == 0 || + strcmp(slot_value, "none") == 0); +} + +/** + * @brief Resolve a legacy slot value to matching sensor entry. + * @details Maps "cpu"→first CPU temp, "gpu"→first GPU temp, + * "liquid"→first Liquidctl temp. + */ +static const sensor_entry_t *resolve_legacy_slot( + const monitor_sensor_data_t *data, const char *slot_value) +{ + if (!data || !slot_value) + return NULL; + + const char *target_type = NULL; + if (strcmp(slot_value, "cpu") == 0) + target_type = "CPU"; + else if (strcmp(slot_value, "gpu") == 0) + target_type = "GPU"; + else if (strcmp(slot_value, "liquid") == 0) + target_type = "Liquidctl"; + else + return NULL; + + /* Find first temperature sensor matching the device type */ + for (int i = 0; i < data->sensor_count; i++) + { + if (data->sensors[i].category == SENSOR_CATEGORY_TEMP && + strcmp(data->sensors[i].device_type, target_type) == 0) + { + return &data->sensors[i]; + } + } + + return NULL; +} + +/** + * @brief Find sensor entry matching a slot value. + * @details Handles both legacy ("cpu","gpu","liquid") and dynamic + * ("uid:sensor_name") slot resolution. + */ +const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data, + const char *slot_value) +{ + if (!data || !slot_value || strcmp(slot_value, "none") == 0) + return NULL; + + /* Legacy slot resolution */ + if (is_legacy_sensor_slot(slot_value)) + return resolve_legacy_slot(data, slot_value); + + /* Dynamic slot: "device_uid:sensor_name" */ + const char *separator = strchr(slot_value, ':'); + if (!separator || separator == slot_value) + return NULL; + + size_t uid_len = (size_t)(separator - slot_value); + const char *sensor_name = separator + 1; + + for (int i = 0; i < data->sensor_count; i++) + { + if (strlen(data->sensors[i].device_uid) == uid_len && + strncmp(data->sensors[i].device_uid, slot_value, uid_len) == 0 && + strcmp(data->sensors[i].name, sensor_name) == 0) + { + return &data->sensors[i]; + } + } + + return NULL; +} + +// ============================================================================ +// Public API +// ============================================================================ + +/** + * @brief Get all sensor data from CoolerControl API. + */ +int get_sensor_monitor_data(const Config *config, monitor_sensor_data_t *data) { - // Check if config and data pointers are valid if (!config || !data) return 0; - // Get temperature data from monitor module - return get_temperature_data(config, &data->temp_cpu, &data->temp_gpu, - &data->temp_liquid); + return get_sensor_data_from_api(config, data); } diff --git a/src/srv/cc_sensor.h b/src/srv/cc_sensor.h index 9158602..6545875 100644 --- a/src/srv/cc_sensor.h +++ b/src/srv/cc_sensor.h @@ -8,15 +8,15 @@ */ /** - * @brief CPU/GPU temperature monitoring via CoolerControl API. - * @details Reads sensor data from /status endpoint. + * @brief Sensor monitoring via CoolerControl API. + * @details Reads all sensor data (temperatures, RPM, duty, watts, frequency) + * from /status endpoint for all device types (CPU, GPU, Liquidctl, Hwmon, + * CustomSensors). */ -// Include necessary headers #ifndef CC_SENSOR_H #define CC_SENSOR_H -// Include necessary headers // cppcheck-suppress-begin missingIncludeSystem #include // cppcheck-suppress-end missingIncludeSystem @@ -24,25 +24,105 @@ // Forward declaration struct Config; +// ============================================================================ +// Sensor Data Model Constants +// ============================================================================ + +#define MAX_SENSORS 64 +#define SENSOR_NAME_LEN 48 +#define SENSOR_UID_LEN 96 +#define SENSOR_DEVICE_NAME_LEN 64 +#define SENSOR_DEVICE_TYPE_LEN 16 +#define SENSOR_UNIT_LEN 8 + +// ============================================================================ +// Sensor Category Enum +// ============================================================================ + +/** + * @brief Category of a sensor value. + * @details Determines default thresholds, display formatting, and unit. + */ +typedef enum +{ + SENSOR_CATEGORY_TEMP = 0, /**< Temperature in °C */ + SENSOR_CATEGORY_RPM, /**< Fan/Pump speed in RPM */ + SENSOR_CATEGORY_DUTY, /**< Duty cycle in % */ + SENSOR_CATEGORY_WATTS, /**< Power consumption in W */ + SENSOR_CATEGORY_FREQ /**< Frequency in MHz */ +} sensor_category_t; + +// ============================================================================ +// Sensor Entry +// ============================================================================ + +/** + * @brief Single sensor data entry from CoolerControl API. + * @details Represents one sensor value with its metadata. Names match + * CoolerControl's display names for maximum UI synchronicity. + */ +typedef struct +{ + char name[SENSOR_NAME_LEN]; /**< CC sensor name (e.g. "temp1", "Liquid Temperature") */ + char device_uid[SENSOR_UID_LEN]; /**< CC device UID */ + char device_name[SENSOR_DEVICE_NAME_LEN]; /**< CC device name (e.g. "NZXT Kraken Z73") */ + char device_type[SENSOR_DEVICE_TYPE_LEN]; /**< CC device type ("CPU","GPU","Liquidctl","Hwmon","CustomSensors") */ + sensor_category_t category; /**< Sensor value category */ + float value; /**< Current sensor value */ + char unit[SENSOR_UNIT_LEN]; /**< Display unit ("°C","RPM","%","W","MHz") */ + int use_decimal; /**< 1=show decimal (e.g. 31.5), 0=integer (e.g. 1200) */ +} sensor_entry_t; + +// ============================================================================ +// Monitor Sensor Data (runtime collection) +// ============================================================================ + /** - * @brief Structure to hold temperature sensor data. - * @details Contains temperature values (CPU, GPU, and Liquid/Coolant) - * representing temperatures in degrees Celsius. + * @brief Collection of all sensor values from one API poll. + * @details Contains all discovered sensors across all CoolerControl devices. */ typedef struct { - float temp_cpu; - float temp_gpu; - float temp_liquid; // Liquid/Coolant temperature from Liquidctl device + sensor_entry_t sensors[MAX_SENSORS]; /**< Array of all discovered sensors */ + int sensor_count; /**< Number of valid entries in sensors[] */ } monitor_sensor_data_t; +// ============================================================================ +// Sensor Slot Resolution Functions +// ============================================================================ + +/** + * @brief Find sensor entry matching a slot value. + * @details Resolves legacy ("cpu","gpu","liquid") and dynamic ("uid:name") + * slot values to the corresponding sensor_entry_t in the data array. + * @param data Current sensor data collection + * @param slot_value Slot configuration value + * @return Pointer to matching sensor entry, or NULL if not found + */ +const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data, + const char *slot_value); + +/** + * @brief Check if a slot value is a legacy type. + * @param slot_value Slot configuration value + * @return 1 if "cpu", "gpu", "liquid", or "none"; 0 otherwise + */ +int is_legacy_sensor_slot(const char *slot_value); + +// ============================================================================ +// Data Retrieval +// ============================================================================ + /** - * @brief Get temperature data into structure. - * @details High-level convenience function that retrieves temperature data and - * populates a monitor_sensor_data_t structure with the values. + * @brief Get all sensor data from CoolerControl API. + * @details Polls /status endpoint and collects all sensors (temps + channels) + * from all device types into the monitor_sensor_data_t structure. + * @param config Configuration with daemon address + * @param data Output sensor data structure + * @return 1 on success, 0 on failure */ -int get_temperature_monitor_data(const struct Config *config, - monitor_sensor_data_t *data); +int get_sensor_monitor_data(const struct Config *config, + monitor_sensor_data_t *data); /** * @brief Cleanup cached sensor CURL handle. From 905f2558944e48e718b5ebeb7465b64131ce4ccb Mon Sep 17 00:00:00 2001 From: damachine Date: Sat, 14 Feb 2026 03:05:32 +0100 Subject: [PATCH 6/8] fix: codeql massage in xss vulnerability --- .../plugins/coolerdash/ui/index.html | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index 7a8effa..49c6ddf 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -1188,6 +1188,19 @@

System Environment

let discoveredSensors = []; let currentSensorConfig = {}; + /** + * @brief Escape HTML special characters to prevent XSS. + */ + function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + // ===== FALLBACK FUNCTIONS ===== const isInCoolerControl = window.parent !== window; @@ -1509,8 +1522,10 @@

System Environment

var sensorId = sensorIds[si]; var sc = sensors[sensorId]; var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_'); - var displayName = getSensorDisplayName(sensorId); + var displayName = escapeHtml(getSensorDisplayName(sensorId)); var defs = getDefaultSensorConfig(sensorId); + var eSensorId = escapeHtml(sensorId); + var eLabel = escapeHtml(sc.label || ''); var section = document.createElement('div'); section.className = 'sensor-config-section'; @@ -1527,22 +1542,22 @@

System Environment

'
' + '' + 'Custom label (empty = auto)' + - '' + + '' + '
' + '
' + '' + 'Default: 100 (0 = global)' + - '' + + '' + '
' + '
' + '' + 'Default: 0' + - '' + + '' + '
' + '
' + '' + 'Default: 0' + - '' + + '' + '
' + '' + @@ -1552,22 +1567,22 @@

System Environment

'
' + '' + 'Default: ' + defs.threshold_1 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.threshold_2 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.threshold_3 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.max_scale + '' + - '' + + '' + '
' + '' + From f97cdc3af53677f8a6e952580f9692077607e277 Mon Sep 17 00:00:00 2001 From: damachine Date: Sat, 14 Feb 2026 17:04:00 +0100 Subject: [PATCH 7/8] fix plugin-ui --- .../plugins/coolerdash/ui/index.html | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index 49c6ddf..8baa669 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -1527,6 +1527,15 @@

System Environment

var eSensorId = escapeHtml(sensorId); var eLabel = escapeHtml(sc.label || ''); + // Sanitize all numeric values to break taint chain + var eFontSize = Number(sc.font_size_temp) || 0; + var eOffsetX = Number(sc.offset_x) || 0; + var eOffsetY = Number(sc.offset_y) || 0; + var eThresh1 = Number(sc.threshold_1 !== undefined ? sc.threshold_1 : defs.threshold_1); + var eThresh2 = Number(sc.threshold_2 !== undefined ? sc.threshold_2 : defs.threshold_2); + var eThresh3 = Number(sc.threshold_3 !== undefined ? sc.threshold_3 : defs.threshold_3); + var eMaxScale = Number(sc.max_scale !== undefined ? sc.max_scale : defs.max_scale); + var section = document.createElement('div'); section.className = 'sensor-config-section'; section.setAttribute('data-sensor-id', sensorId); @@ -1547,17 +1556,17 @@

System Environment

'
' + '' + 'Default: 100 (0 = global)' + - '' + + '' + '
' + '
' + '' + 'Default: 0' + - '' + + '' + '
' + '
' + '' + 'Default: 0' + - '' + + '' + '
' + '' + @@ -1567,22 +1576,22 @@

System Environment

'
' + '' + 'Default: ' + defs.threshold_1 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.threshold_2 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.threshold_3 + '' + - '' + + '' + '
' + '
' + '' + 'Default: ' + defs.max_scale + '' + - '' + + '' + '
' + '' + @@ -1682,13 +1691,18 @@

System Environment

var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_'); var sc = {}; - // Threshold/offset fields + // Threshold/offset fields (sanitize: numbers stay numbers, strings get escaped) var fields = section.querySelectorAll('.sensor-field'); for (var f = 0; f < fields.length; f++) { var field = fields[f].getAttribute('data-field'); if (field) { - var numVal = parseFloat(fields[f].value); - sc[field] = isNaN(numVal) ? fields[f].value : numVal; + if (field === 'label') { + // Label is the only string field - store as-is (escaped on render) + sc[field] = String(fields[f].value || ''); + } else { + // All other fields are numeric - force to number + sc[field] = Number(fields[f].value) || 0; + } } } From 61e354e636966c242a286a99d68ced3791d63555 Mon Sep 17 00:00:00 2001 From: damachine Date: Sun, 15 Feb 2026 01:07:22 +0100 Subject: [PATCH 8/8] ui: elemtents better aligned and more compact --- .../plugins/coolerdash/ui/index.html | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index 8baa669..f97a438 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -483,6 +483,10 @@ overflow: hidden; } + .collapsible-details + .collapsible-details { + margin-top: 12px; + } + .sensor-details summary, .collapsible-details summary { padding: 12px 16px; @@ -704,9 +708,7 @@

Display Geometry

- -
0 = automatic @@ -718,9 +720,7 @@

Display Geometry

0 = automatic
-
-
Default: 0.98 @@ -738,9 +738,10 @@

Display Geometry

-
- Bar Dimensions +
+ Bars
+

Dimensions

@@ -758,12 +759,8 @@

Display Geometry

-
-
-
- Individual Bar Heights -
+

Individual Heights

Set to 0 to use default bar height
@@ -782,12 +779,8 @@

Display Geometry

-
-
-
- Border & Margins -
+

Border

@@ -802,23 +795,14 @@

Display Geometry

Default: 1.0
-
- - Default: 1 - -
-
- - Default: 1 - -
-
- Font +
+ Labels & Text
+

Font

@@ -836,12 +820,22 @@

Display Geometry

-
-
-
- Global Positioning -
+

Margins

+
+
+ + Default: 1 + +
+
+ + Default: 1 + +
+
+ +

Positioning

Global Offsets These offsets apply to all labels globally. Per-sensor value offsets are configured in the Sensors tab.