From 0dd30fc1f9c7b525fae0a168670d01474dcb8c1a Mon Sep 17 00:00:00 2001 From: damachin3 Date: Tue, 7 Apr 2026 23:20:46 +0200 Subject: [PATCH 1/4] prepare to release 3.0.3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b502146..75a22a2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.2 +3.0.3 From b539d8cba91fc5c1c202b4c3402c11d20f90004c Mon Sep 17 00:00:00 2001 From: damachin3 Date: Wed, 8 Apr 2026 22:16:26 +0200 Subject: [PATCH 2/4] VERSION: bump to 3.0.3 Major circle mode enhancements, resolution-aware layout refactoring, improved device detection, authentication cleanup, and sensor data improvements. --- .SRCINFO | 2 +- docs/config-guide.md | 23 +- docs/coolercontrol-api.md | 122 +--- docs/developer-guide.md | 53 +- docs/display-modes.md | 6 +- docs/plugin-integration.md | 4 +- docs/plugin-ui-theming.md | 2 +- .../plugins/coolerdash/config.json | 40 +- .../plugins/coolerdash/ui/index.html | 596 ++++++++++-------- src/device/config.c | 342 +++++----- src/device/config.h | 27 +- src/main.c | 19 +- src/mods/circle.c | 555 +++++++++++++--- src/mods/display.c | 547 +++++++++++++--- src/mods/display.h | 110 +++- src/mods/dual.c | 546 ++++++++-------- src/srv/cc_conf.c | 288 ++++++++- src/srv/cc_main.c | 208 +----- src/srv/cc_main.h | 20 +- src/srv/cc_sensor.c | 68 +- src/srv/cc_sensor.h | 14 + 21 files changed, 2285 insertions(+), 1307 deletions(-) diff --git a/.SRCINFO b/.SRCINFO index 5ae15e2..c117e68 100644 --- a/.SRCINFO +++ b/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = coolerdash pkgdesc = Plug-in for CoolerControl that extends the LCD functionality with additional features - pkgver = 3.0.2 + pkgver = 3.0.3 pkgrel = 1 url = https://github.com/damachine/coolerdash install = coolerdash.install diff --git a/docs/config-guide.md b/docs/config-guide.md index 2a5a032..7969f39 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -24,19 +24,16 @@ Connection to the CoolerControl daemon. ```json "daemon": { "address": "http://localhost:11987", - "access_token": "", - "password": "coolAdmin" + "access_token": "" } ``` | Key | Default | Description | |-----|---------|-------------| | `address` | `http://localhost:11987` | API endpoint | -| `access_token` | `""` | **CC4:** Bearer token from CoolerControl UI → Access Protection. Format: `cc_`. Takes precedence over `password`. | -| `password` | `coolAdmin` | **CC3 / fallback.** Ignored when `access_token` is set. | +| `access_token` | `""` | Bearer token from CoolerControl UI → Access Protection. Format: `cc_`. Required for authenticated API access. | -> CC4: generate a token in CoolerControl UI under **Access Protection** and paste it into `access_token`. -> CC3: leave `access_token` empty, set `password`. +Generate a token in CoolerControl UI under **Access Protection** and paste it into `access_token`. --- @@ -92,9 +89,9 @@ LCD display configuration tested with NZXT Kraken 2023. - **`circle_switch_interval`**: Slot switch interval for circle mode in seconds (1–60, default: `5`) - **`content_scale_factor`**: Safe area percentage (0.5–1.0, default: `0.98`) - **`inscribe_factor`**: Inscribe factor for circular displays (default: `0.70710678` = 1/√2) -- **`sensor_slot_up`**: Sensor shown in top slot: `cpu`, `gpu`, or `liquid` (default: `cpu`) -- **`sensor_slot_mid`**: Sensor shown in middle slot (default: `liquid`) -- **`sensor_slot_down`**: Sensor shown in bottom slot (default: `gpu`) +- **`sensor_slot_1`**: Sensor for slot 1: `cpu`, `gpu`, or `liquid` (default: `cpu`) +- **`sensor_slot_2`**: Sensor for slot 2 (default: `liquid`) +- **`sensor_slot_3`**: Sensor for slot 3 (default: `gpu`) Sensor slots control which sensor appears in each display position for both dual and circle mode. @@ -197,9 +194,9 @@ All values are in pixels unless noted. Positions are calculated dynamically from "bar_border_enabled": 1, "label_margin_left": 1, "label_margin_bar": 1, - "bar_height_up": 0, - "bar_height_mid": 0, - "bar_height_down": 0 + "bar_height_1": 0, + "bar_height_2": 0, + "bar_height_3": 0 } ``` @@ -212,7 +209,7 @@ All values are in pixels unless noted. Positions are calculated dynamically from | `bar_border_enabled` | `1` | Enable bar border (`1`/`0`) | | `label_margin_left` | `1` | Left label margin multiplier | | `label_margin_bar` | `1` | Margin between label and bar | -| `bar_height_up/mid/down` | `0` | Per-slot bar height override. `0` = use `bar_height` | +| `bar_height_1/2/3` | `0` | Per-slot bar height override. `0` = use `bar_height` | --- diff --git a/docs/coolercontrol-api.md b/docs/coolercontrol-api.md index 6373841..25b4cdb 100644 --- a/docs/coolercontrol-api.md +++ b/docs/coolercontrol-api.md @@ -29,7 +29,6 @@ CoolerDash acts as a specialized LCD client for CoolerControl, fetching temperat ### API Endpoints Used ``` -POST /login - CC3 authentication (Basic Auth cookie) GET /devices - Device enumeration POST /status - Temperature sensor data PUT /devices/{uid}/settings/lcd/lcd/images - LCD image upload @@ -147,50 +146,32 @@ cc_cleanup_response_buffer(&response); #### 2. Session State -**Purpose**: Maintain persistent CURL session with cookie-based authentication +**Purpose**: Maintain persistent CURL session with Bearer-token authentication ```c typedef struct { CURL *curl_handle; - char cookie_jar[CC_COOKIE_SIZE]; + char access_token[CC_BEARER_HEADER_SIZE]; int session_initialized; } CoolerControlSession; static CoolerControlSession cc_session = { .curl_handle = NULL, - .cookie_jar = {0}, + .access_token = {0}, .session_initialized = 0 }; ``` -**Cookie Jar**: `/tmp/coolerdash_cookie_{PID}.txt` -- Stores session cookies for authenticated requests -- Cleaned up on program exit -- Per-process isolation via PID - #### 3. Authentication Flow **Function**: `init_coolercontrol_session(const Config *config)` -**CC4 — Bearer Token (primary):** -- If `access_token` is set in config, all requests use `Authorization: Bearer cc_` -- No cookie jar, no `/login` call needed -- Token is generated in CoolerControl UI under **Access Protection** +CoolerDash uses one authentication mode only: -**CC3 — Basic Auth cookie (fallback):** -- Used when `access_token` is empty -- Sends `POST /login` with HTTP Basic Auth (`CCAdmin:{password}`) -- Cookie jar stored at `/tmp/coolerdash_cookie_{PID}.txt` (in-memory, written only on cleanup, deleted immediately after) - -**Steps (CC3 path)**: -1. Initialize libcurl global state -2. Create CURL easy handle -3. Configure cookie jar for session persistence -4. Build login URL: `{daemon_address}/login` -5. Set HTTP Basic Auth: `CCAdmin:{password}` -6. Send POST request with empty body -7. Validate response (200/204) -8. Store cookies for subsequent requests +- `access_token` must be set in config +- All requests use `Authorization: Bearer cc_` +- Token is generated in CoolerControl UI under **Access Protection** +- No legacy login or session fallback is used --- @@ -203,55 +184,28 @@ Called **once at startup** after device initialization. Registers `shutdown.png` - Endpoint: `PUT {address}/devices/{uid}/settings/lcd/lcd/shutdown-image` - Same multipart form as live image upload - CC4 stores image server-side; displays it when CoolerControl stops -- Returns gracefully on 404 (CC3 — endpoint does not exist) +- Returns gracefully on 404 when the endpoint is unavailable **Implementation**: ```c int init_coolercontrol_session(const Config *config) { + if (!config || config->access_token[0] == '\0') + return 0; + curl_global_init(CURL_GLOBAL_DEFAULT); cc_session.curl_handle = curl_easy_init(); - - // Cookie jar: /tmp/coolerdash_cookie_{PID}.txt - snprintf(cc_session.cookie_jar, sizeof(cc_session.cookie_jar), - "/tmp/coolerdash_cookie_%d.txt", getpid()); - - curl_easy_setopt(cc_session.curl_handle, CURLOPT_COOKIEJAR, cc_session.cookie_jar); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_COOKIEFILE, cc_session.cookie_jar); - - // Login request - char login_url[CC_URL_SIZE]; - snprintf(login_url, sizeof(login_url), "%s/login", config->daemon_address); - - char userpwd[CC_USERPWD_SIZE]; - snprintf(userpwd, sizeof(userpwd), "CCAdmin:%s", config->daemon_password); - - curl_easy_setopt(cc_session.curl_handle, CURLOPT_URL, login_url); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_USERPWD, userpwd); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_POST, 1L); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_POSTFIELDS, ""); - - CURLcode res = curl_easy_perform(cc_session.curl_handle); - long response_code = 0; - curl_easy_getinfo(cc_session.curl_handle, CURLINFO_RESPONSE_CODE, &response_code); - - // Secure password cleanup - memset(userpwd, 0, sizeof(userpwd)); - - if (res == CURLE_OK && (response_code == 200 || response_code == 204)) { - cc_session.session_initialized = 1; - return 1; - } - - return 0; + + snprintf(cc_session.access_token, sizeof(cc_session.access_token), + "Authorization: Bearer %s", config->access_token); + cc_session.session_initialized = 1; + return 1; } ``` **Security Notes**: -- Password immediately zeroed after use (`memset`) -- HTTPS support with SSL verification -- Cookie-based session prevents password re-transmission +- Bearer token is attached explicitly to every authenticated request +- No fallback authentication path exists #### 4. LCD Image Upload @@ -353,8 +307,8 @@ int send_image_to_lcd(const Config *config, const char *image_path, const char * **Steps**: 1. Cleanup CURL easy handle 2. Cleanup libcurl global state -3. Remove cookie jar file -4. Mark session as uninitialized +3. Mark session as uninitialized +4. Clear the cached Bearer header 5. Set cleanup flag to prevent double-cleanup **Implementation**: @@ -373,12 +327,10 @@ void cleanup_coolercontrol_session(void) // Global CURL cleanup curl_global_cleanup(); - - // Remove cookie jar - unlink(cc_session.cookie_jar); - + // Mark as uninitialized cc_session.session_initialized = 0; + cc_session.access_token[0] = '\0'; cleanup_done = 1; } ``` @@ -981,11 +933,7 @@ main() startup ↓ 2. init_coolercontrol_session(&config) │ - ├─→ POST /login - │ (HTTP Basic Auth: CCAdmin:{password}) - │ Response: 200 OK + session cookie - │ - └─→ Save cookie to /tmp/coolerdash_cookie_{PID}.txt + └─→ Build Authorization: Bearer cc_ ↓ 3. init_device_cache(&config) │ @@ -1074,7 +1022,7 @@ Program exit struct Config { // CoolerControl connection char daemon_address[256]; // e.g., "http://127.0.0.1:11987" - char daemon_password[128]; // CCAdmin password + char access_token[64]; // Bearer token // Display settings uint16_t display_width; // e.g., 640 (auto-detected if 0) @@ -1111,7 +1059,7 @@ static struct { ```c typedef struct { CURL *curl_handle; // Persistent CURL handle - char cookie_jar[CC_COOKIE_SIZE]; // Cookie file path + char access_token[CC_BEARER_HEADER_SIZE]; // Authorization header int session_initialized; // 0 = not initialized, 1 = ready } CoolerControlSession; @@ -1163,18 +1111,19 @@ LOG_STATUS - Normal operation status (upload success) **Symptom**: `init_coolercontrol_session()` returns 0 **Causes**: -- Wrong password in config +- Missing or invalid access token - CoolerControl daemon not running - Daemon address unreachable **Debugging**: ```c -log_message(LOG_ERROR, "Login failed: CURL code %d, HTTP code %ld", res, response_code); +log_message(LOG_ERROR, + "CoolerControl access token missing; token-only authentication is required"); ``` **Resolution**: - Verify daemon is running: `systemctl status coolercontrol` -- Check password in `config.json` +- Check `access_token` in `config.json` - Test connection: `curl http://127.0.0.1:11987/devices` #### 2. Device Not Found @@ -1411,10 +1360,9 @@ if (response.data && response.size > 0) { **Test authentication**: ```bash -curl -X POST http://127.0.0.1:11987/login \ - -u CCAdmin:your_password \ - -c cookies.txt \ - -v +curl http://127.0.0.1:11987/devices \ + -H "Authorization: Bearer cc_" \ + -v ``` **Test device list**: @@ -1506,7 +1454,7 @@ cat /tmp/coolerdash_cookie_*.txt **Optimization**: - Device cache eliminates repeated /devices calls - Persistent CURL session reuses HTTP connection -- Cookie-based auth avoids re-login +- Shared Bearer header avoids rebuilding auth state on every request ### Memory Usage @@ -1560,7 +1508,7 @@ cat /tmp/coolerdash_cookie_*.txt ## Conclusion The CoolerControl API integration provides a robust, efficient interface for: -- **Authentication**: Cookie-based session with HTTP Basic Auth +- **Authentication**: Bearer token via CoolerControl Access Protection - **Device Detection**: Automatic CoolerControl LCD device discovery and caching - **Temperature Monitoring**: Real-time CPU/GPU data retrieval - **LCD Upload**: Multipart image upload with brightness/orientation control diff --git a/docs/developer-guide.md b/docs/developer-guide.md index e3512e9..2905344 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -32,7 +32,7 @@ CoolerDash extends the LCD functionality of [CoolerControl](https://gitlab.com/c - **Real-time Temperature Monitoring:** CPU/GPU/liquid sensor data via CoolerControl REST API - **Adaptive Display Rendering:** Automatic circular/rectangular display detection - **Customizable UI:** Full color/layout/font/sensor-slot configuration via `config.json` -- **CC4 Authentication:** Bearer Token (primary) with CC3 Basic Auth cookie fallback +- **Authentication:** Bearer Token via CoolerControl Access Protection - **Shutdown Image:** Registered with CC4 at startup — CC handles display on daemon stop - **Efficient Caching:** One-time device information retrieval at startup - **CoolerControl Plugin:** Managed by `cc-plugin-coolerdash.service`, no separate systemd service needed @@ -46,7 +46,7 @@ CoolerDash extends the LCD functionality of [CoolerControl](https://gitlab.com/c - `jansson` — JSON parsing (config + API) - `libcurl-gnutls` — HTTP client - `ttf-roboto` — Font rendering -- **Required Service:** CoolerControl >=4.x recommended (CC3 supported as fallback) +- **Required Service:** CoolerControl >=4.x --- @@ -60,8 +60,7 @@ CoolerDash extends the LCD functionality of [CoolerControl](https://gitlab.com/c │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 1. Configuration Loading (device/config.c) │ │ │ │ 2. Session Initialization (cc_main.c) │ │ -│ │ ├─ CC4: Bearer Token header │ │ -│ │ └─ CC3: Basic Auth cookie via /login │ │ +│ │ └─ Bearer Token header │ │ │ │ 3. Device Cache Setup (cc_conf.c) │ │ │ │ 4. Shutdown Image Registration (cc_main.c, CC4 only) │ │ │ │ 5. Main Loop (configurable interval) │ │ @@ -235,7 +234,7 @@ LIBS = $(shell pkg-config --libs cairo jansson libcurl) -lm typedef struct { // Daemon connection char daemon_address[256]; // CoolerControl API URL - char daemon_password[128]; // API password + char access_token[64]; // Bearer token (cc_) // File paths char paths_images[PATH_MAX]; // Shutdown image directory @@ -314,21 +313,14 @@ void log_config(const Config *config); // Uses LOG_STATUS level (always visible ### CoolerControl REST API **Base URL:** `http://localhost:11987` (configurable) -**Authentication:** HTTP Basic Auth (username: CCAdmin) -**Session Management:** Cookie-based (stored in `/tmp/coolerdash_cookie_.txt`) +**Authentication:** Bearer token via `Authorization: Bearer cc_` +**Session Management:** Single initialized CURL session with shared auth header ### API Endpoints Used -#### 1. Login & Session +#### 1. Session Setup -```http -POST /login -Authorization: Basic CCAdmin: -Content-Type: application/json - -Response: 200 OK / 204 No Content -Sets: session cookies -``` +CoolerDash initializes one CURL session and stores the Bearer header built from the configured access token. **Implementation:** `src/srv/cc_main.c` → `init_coolercontrol_session()` @@ -449,25 +441,23 @@ Response: 200 OK ┌─────────────────────────────────────────────────────────────┐ │ 1. init_coolercontrol_session() │ │ ├─ curl_global_init() │ -│ ├─ POST /login (HTTP Basic Auth) │ -│ ├─ Store cookies in /tmp/coolerdash_cookie_.txt │ +│ ├─ curl_easy_init() │ +│ ├─ Build Authorization: Bearer cc_ header │ │ └─ Set session_initialized = 1 │ ├─────────────────────────────────────────────────────────────┤ -│ 2. Repeated API Calls (use existing cookies) │ -│ ├─ send_image_to_lcd() - every 60s │ -│ └─ get_temperature_data() - every 60s │ +│ 2. Repeated API Calls (reuse initialized session) │ +│ ├─ send_image_to_lcd() - every 60s │ +│ └─ get_temperature_data() - every 60s │ ├─────────────────────────────────────────────────────────────┤ │ 3. cleanup_coolercontrol_session() │ │ ├─ curl_easy_cleanup() │ │ ├─ curl_global_cleanup() │ -│ ├─ unlink(cookie_jar) │ │ └─ Set session_initialized = 0 │ └─────────────────────────────────────────────────────────────┘ ``` **Security Features:** -- Cookie jar path includes PID (prevents conflicts) -- SSL verification enabled for HTTPS endpoints +- Explicit Bearer auth header on each API request - Cleanup protection with static flag (prevents double-free) --- @@ -680,7 +670,7 @@ int calculate_temp_fill_width(float temp, int max_width, float max_temp) { ```c daemon_address = "http://localhost:11987" -daemon_password = "coolAdmin" +access_token = "" display_width = 0 // auto-detected from API display_refresh_interval = 2.5 lcd_brightness = 80 @@ -690,7 +680,7 @@ lcd_brightness = 80 ```json { - "daemon": { "address": "...", "password": "..." }, + "daemon": { "address": "...", "access_token": "cc_" }, "display": { "width": 0, "height": 0, "brightness": 80, "mode": "dual" }, "layout": { "bar_height": 30, "label_size": 18, "value_size": 24 }, "font": { "face": "Roboto" }, @@ -706,9 +696,9 @@ lcd_brightness = 80 | Function | Purpose | Returns | |----------|---------|---------| -| `init_coolercontrol_session(config)` | Login, setup cookie jar | `int` success | +| `init_coolercontrol_session(config)` | Initialize CURL session and Bearer header | `int` success | | `is_session_initialized()` | Check session state | `int` boolean | -| `cleanup_coolercontrol_session()` | Cleanup CURL, delete cookies | `void` | +| `cleanup_coolercontrol_session()` | Cleanup CURL session resources | `void` | | `send_image_to_lcd(config, image_path, device_uid)` | Upload PNG to LCD | `int` success | #### Internal Helpers (15 functions) @@ -1110,11 +1100,8 @@ sudo systemctl restart coolerdash.service # Check CoolerControl status systemctl status coolercontrold -# Test API manually -curl -u CCAdmin:coolAdmin -X POST http://localhost:11987/login - -# Check config password -grep daemon_password /etc/coolercontrol/plugins/coolerdash/config.json +# Check configured access token +grep access_token /etc/coolercontrol/plugins/coolerdash/config.json ``` --- diff --git a/docs/display-modes.md b/docs/display-modes.md index e88b3fc..c7fcc12 100644 --- a/docs/display-modes.md +++ b/docs/display-modes.md @@ -172,9 +172,9 @@ Circle mode cycles through the three sensor slots configured in `config.json`: ```json "display": { - "sensor_slot_up": "cpu", - "sensor_slot_mid": "liquid", - "sensor_slot_down": "gpu" + "sensor_slot_1": "cpu", + "sensor_slot_2": "liquid", + "sensor_slot_3": "gpu" } ``` diff --git a/docs/plugin-integration.md b/docs/plugin-integration.md index c5d2f50..d513a2e 100644 --- a/docs/plugin-integration.md +++ b/docs/plugin-integration.md @@ -16,8 +16,6 @@ CC4 uses Bearer Token authentication. Generate a token in CoolerControl UI under } ``` -CC3 fallback: leave `access_token` empty, set `password` instead. - ### Shutdown Image CoolerDash registers `shutdown.png` with CC4 once at startup: @@ -96,7 +94,7 @@ The plugin settings page includes: ### 🌐 Daemon Settings - CoolerControl API Address -- API Password +- Access Token ### 🖥️ Display Mode - Mode selection (Dual/Circle) diff --git a/docs/plugin-ui-theming.md b/docs/plugin-ui-theming.md index e95006f..2b38863 100644 --- a/docs/plugin-ui-theming.md +++ b/docs/plugin-ui-theming.md @@ -186,6 +186,6 @@ These will be displayed on the plugin page in CoolerControl's UI. ## Changelog -- **3.x** - CC4 Bearer Token auth, native shutdown image via CC4 API +- **3.x** - Token-only CC4 auth, native shutdown image via CC4 API - **2.2.x** - Added theme color support and Tailwind CSS integration - **2.0.4** - Initial plugin UI implementation diff --git a/etc/coolercontrol/plugins/coolerdash/config.json b/etc/coolercontrol/plugins/coolerdash/config.json index 29b639a..9d3c34e 100644 --- a/etc/coolercontrol/plugins/coolerdash/config.json +++ b/etc/coolercontrol/plugins/coolerdash/config.json @@ -4,9 +4,13 @@ "daemon": { "address": "http://localhost:11987", "access_token": "", - "_comment_token": "CC4: Create token in UI → Access Protection. Format: cc_. Preferred over password.", - "password": "coolAdmin", - "_comment_password": "Legacy CC3 / fallback. Ignored when access_token is set." + "_comment_token": "Create a token in CoolerControl UI → Access Protection. Format: cc_." + }, + + "device_detection": { + "mode": "strict", + "allowlist": "", + "blocklist": "mainboard\nmotherboard\nchipset" }, "paths": { @@ -17,8 +21,9 @@ }, "display": { - "mode": "dual", + "mode": "circle", "circle_switch_interval": 5, + "circle_show_extra_info": true, "refresh_interval": 2.5, "brightness": 80, "orientation": 0, @@ -26,12 +31,10 @@ "background_overlay_opacity": 0.0, "width": 0, "height": 0, - "shape": "auto", "content_scale_factor": 0.98, - "inscribe_factor": 0.70710678, - "sensor_slot_up": "cpu", - "sensor_slot_mid": "liquid", - "sensor_slot_down": "gpu" + "sensor_slot_1": "cpu", + "sensor_slot_2": "liquid", + "sensor_slot_3": "gpu" }, "layout": { @@ -43,9 +46,9 @@ "bar_border_enabled": 1, "label_margin_left": 1, "label_margin_bar": 1, - "bar_height_up": 0, - "bar_height_mid": 0, - "bar_height_down": 0 + "bar_height_1": 0, + "bar_height_2": 0, + "bar_height_3": 0 }, "colors": { @@ -57,8 +60,9 @@ "font": { "face": "Roboto Black", - "size_temp": 100.0, - "size_labels": 30.0 + "size_temp": 0.0, + "size_labels": 0.0, + "font_growth_factor": 1.33 }, "sensors": { @@ -87,10 +91,10 @@ "offset_y": 0 }, "liquid": { - "threshold_1": 25.0, - "threshold_2": 28.0, - "threshold_3": 31.0, - "max_scale": 50.0, + "threshold_1": 30.0, + "threshold_2": 35.0, + "threshold_3": 40.0, + "max_scale": 55.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 }, diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index b65e5ef..d111925 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -334,6 +334,7 @@ .range-input { flex: 1; height: 6px; + appearance: none; background: var(--bg-input); border-radius: 3px; outline: none; @@ -582,7 +583,7 @@

CoolerDash Configuration

- HTTP or HTTPS supported (Default: http://localhost:11987) + Default: http://localhost:11987
@@ -599,36 +600,9 @@

Authentication — CC4 (Bearer Token)

- Enter a CC4 Bearer token (cc_…). Leave empty to fall back to password auth (Legacy / CC3). + Enter a CC4 Bearer token (cc_…).
- -

Authentication — Legacy / CC3 (Password)

-
- - Only used when no Access Token is set. Default: coolAdmin - -
- -

TLS / HTTPS (Optional)

-
-
- - CA certificate for self-signed HTTPS (e.g. /etc/coolercontrol/coolercontrol.crt) - -
-
- - Insecure — only use when no CA certificate is available. - -
-
-

- TLS options affect the CoolerDash daemon only — not this browser UI. Localhost always uses plain HTTP. -

@@ -649,6 +623,13 @@

Display Mode

How long each sensor is shown (Default: 5) +

Sensor Slots

@@ -660,9 +641,9 @@

Sensor Slots

- + Default: CPU - @@ -672,10 +653,10 @@

Sensor Slots

-
- - Default: None (Circle only) - @@ -686,9 +667,9 @@

Sensor Slots

- + Default: GPU - @@ -734,25 +715,15 @@

Display Geometry

-
- - Default: Automatic - -
-
- 0 = automatic + 0 = automatic. Live device LCD size from CoolerControl is preferred at runtime; use a fixed value only as fallback/testing override.
- 0 = automatic + 0 = automatic. Live device LCD size from CoolerControl is preferred at runtime; use a fixed value only as fallback/testing override.
@@ -761,12 +732,6 @@

Display Geometry

Default: 0.98
- -
- - Default: 0.707 - -

Background & Shutdown Images

@@ -801,24 +766,29 @@

Background & Shutdown Images

+
+ Resolution-aware layout + Bar height, gap, border thickness, degree spacing, and global offsets use logical base units and are scaled uniformly to the LCD resolution. Width and label margins are percentage-based relative to the safe drawable area. +
+
Bar Dimensions
- Default: 24 - + Logical base units at 240x240. Default: 24 +
- Default: 98 - + Percentage of the safe drawable width. Default: 98 +
- Default: 12 - + Logical base units scaled to the LCD. Default: 12 +
@@ -830,19 +800,19 @@

Background & Shutdown Images

Set to 0 to use default bar height
- - Default: 0 - + + 0 = inherit global bar height +
-
- - Default: 0 - +
+ + 0 = inherit global bar height +
- - Default: 0 - + + 0 = inherit global bar height +
@@ -862,7 +832,7 @@

Background & Shutdown Images

- Default: 1.0 + Logical base units, scaled uniformly. Default: 1.0
@@ -872,12 +842,12 @@

Background & Shutdown Images

- Default: 1 - + Percentage of the safe drawable width. Default: 1 +
- Default: 1 + Percentage of the effective display band height. Default: 1
@@ -895,13 +865,13 @@

Background & Shutdown Images

- Default: 100 (per-sensor override in Sensors tab) - + Default: auto (0 = dynamic, per-sensor override in Sensors tab) +
- Default: 30 - + Default: auto (0 = dynamic) +
@@ -912,23 +882,23 @@

Background & Shutdown Images

Global Offsets - These offsets apply to all labels globally. Per-sensor value offsets are configured in the Sensors tab. + The base layout is dynamic and adapts automatically to the LCD size. Use these offsets only for visual fine-tuning after auto-layout.
- Default: 0 + Logical units, scaled uniformly after auto-layout. Default: 0
- Default: 0 + Logical units, scaled uniformly after auto-layout. Default: 0
- Default: 16 - + Logical units, scaled uniformly. Default: 16 +
@@ -1014,6 +984,42 @@

Backup & Restore

+

LCD Detection

+
+ Configurable LCD selection + CoolerDash scores CoolerControl devices with LCD metadata and picks the best match. Use these rules to keep liquidctl mainboards out or explicitly allow a known LCD by name, type, or UID. +
+
+
+ + Strict is recommended. Relax only if your LCD is not detected automatically. + +
+
+ + Patterns match device name, type, and UID case-insensitively. Use one pattern per line. +
+ Examples: kraken, elite, motherboard, chipset, your-device-uid +
+
+
+
+
+ + Force-enable matching candidates even if strict mode would skip them. + +
+
+ + Reject matching candidates before scoring. Useful for liquidctl mainboards and chipset devices. + +
+
+

Detected LCD Device

@@ -1149,18 +1155,21 @@

System Environment

const FACTORY_DEFAULTS = { daemon: { address: "http://localhost:11987", - access_token: "", - password: "coolAdmin", - tls_ca_cert_path: "", - tls_skip_verify: false + access_token: "" + }, + device_detection: { + mode: "strict", + allowlist: "", + blocklist: "mainboard\nmotherboard\nchipset" }, paths: { image_background: "", image_shutdown: "/etc/coolercontrol/plugins/coolerdash/shutdown.png" }, display: { - mode: "dual", + mode: "circle", circle_switch_interval: 5, + circle_show_extra_info: true, refresh_interval: 2.5, brightness: 80, orientation: 0, @@ -1168,12 +1177,10 @@

System Environment

background_overlay_opacity: 0.0, width: 0, height: 0, - shape: "auto", content_scale_factor: 0.98, - inscribe_factor: 0.70710678, - sensor_slot_up: "cpu", - sensor_slot_mid: "liquid", - sensor_slot_down: "gpu" + sensor_slot_1: "cpu", + sensor_slot_2: "liquid", + sensor_slot_3: "gpu" }, layout: { bar_height: 24, @@ -1184,9 +1191,9 @@

System Environment

bar_border_enabled: 1, label_margin_left: 1, label_margin_bar: 1, - bar_height_up: 0, - bar_height_mid: 0, - bar_height_down: 0 + bar_height_1: 0, + bar_height_2: 0, + bar_height_3: 0 }, colors: { display_background: { r: 0, g: 0, b: 0 }, @@ -1197,8 +1204,8 @@

System Environment

}, font: { face: "Roboto Black", - size_temp: 100.0, - size_labels: 30.0 + size_temp: 0.0, + size_labels: 0.0 }, sensors: { cpu: { @@ -1230,10 +1237,10 @@

System Environment

offset_y: 0 }, liquid: { - threshold_1: 25.0, - threshold_2: 28.0, - threshold_3: 31.0, - max_scale: 50.0, + threshold_1: 30.0, + threshold_2: 35.0, + threshold_3: 40.0, + max_scale: 55.0, font_size_temp: 0, label: '', threshold_1_color: { r: 0, g: 255, b: 0 }, @@ -1252,6 +1259,7 @@

System Environment

}; let DEFAULT_CONFIG = null; + let RESET_CONFIG = null; let currentTab = 0; let discoveredSensors = []; let currentSensorConfig = {}; @@ -1269,6 +1277,10 @@

System Environment

.replace(/'/g, '''); } + function cloneConfig(config) { + return JSON.parse(JSON.stringify(config)); + } + // ===== FALLBACK FUNCTIONS ===== const isInCoolerControl = window.parent !== window; @@ -1329,7 +1341,7 @@

System Environment

// Inside CC4's sandboxed iframe, cc-plugin-lib.js provides the real getDevices() via postMessage. if (typeof getDevices === 'undefined') { window.getDevices = async function() { - var headers = makeAuthHeaders(lastApiToken, lastApiPassword); + var headers = makeAuthHeaders(lastApiToken); var res = await fetch((lastApiAddress || 'http://localhost:11987') + '/devices', { headers: headers }); if (!res.ok) return { devices: [] }; return res.json(); @@ -1339,7 +1351,7 @@

System Environment

// Fallback for getStatus() — used when cc-plugin-lib.js is not loaded (dev/browser mode) if (typeof getStatus === 'undefined') { window.getStatus = async function() { - var headers = makeAuthHeaders(lastApiToken, lastApiPassword); + var headers = makeAuthHeaders(lastApiToken); var res = await fetch((lastApiAddress || 'http://localhost:11987') + '/status', { headers: headers }); if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); @@ -1347,14 +1359,127 @@

System Environment

} // ===== AUTH HEADER HELPER ===== - function makeAuthHeaders(token, password) { + function makeAuthHeaders(token) { var t = (token || '').trim(); - var p = (password || '').trim(); if (t) return { 'Authorization': 'Bearer ' + t }; - if (p) return { 'Authorization': 'Basic ' + btoa('CCAdmin:' + p) }; return {}; } + function splitPatternList(patternList) { + if (!patternList) return []; + return String(patternList) + .split(/[\n,;]+/) + .map(function(item) { return item.trim().toLowerCase(); }) + .filter(function(item) { return item.length > 0; }); + } + + function ciContains(haystack, needle) { + if (!needle) return false; + return String(haystack || '').toLowerCase().indexOf(String(needle).toLowerCase()) !== -1; + } + + function getDetectionSettings(config) { + var section = (config && config.device_detection) || {}; + return { + mode: section.mode || 'strict', + allowlist: splitPatternList(section.allowlist || ''), + blocklist: splitPatternList(section.blocklist || '') + }; + } + + function candidateMatchesPatterns(candidate, patterns) { + if (!patterns || patterns.length === 0) return false; + var name = (candidate.name || '').toLowerCase(); + var type = String(candidate.d_type || candidate.type || '').toLowerCase(); + var uid = String(candidate.uid || '').toLowerCase(); + for (var i = 0; i < patterns.length; i++) { + var pattern = patterns[i]; + if (name.indexOf(pattern) !== -1 || type.indexOf(pattern) !== -1 || uid.indexOf(pattern) !== -1) { + return true; + } + } + return false; + } + + function looksLikeMainboardCandidate(name) { + var lowered = String(name || '').toLowerCase(); + var markers = ['mainboard', 'motherboard', 'chipset', 'vrm', 'z690', 'z790', 'b650', 'b660', 'x670', 'x870', 'x570', 'b550']; + for (var i = 0; i < markers.length; i++) { + if (lowered.indexOf(markers[i]) !== -1) return true; + } + return false; + } + + function getDeviceLcdInfo(device) { + var channels = device && device.info && device.info.channels; + if (!channels) return null; + + var preferredKeys = ['lcd', 'display', 'screen']; + for (var i = 0; i < preferredKeys.length; i++) { + var channel = channels[preferredKeys[i]]; + if (channel && channel.lcd_info && channel.lcd_info.screen_width > 0 && channel.lcd_info.screen_height > 0) { + return channel.lcd_info; + } + } + + for (var key in channels) { + if (channels.hasOwnProperty(key) && channels[key] && channels[key].lcd_info && channels[key].lcd_info.screen_width > 0 && channels[key].lcd_info.screen_height > 0) { + return channels[key].lcd_info; + } + } + + return null; + } + + function scoreDetectedDevice(device, detectionSettings) { + var lcdInfo = getDeviceLcdInfo(device); + if (!device || !device.uid || !lcdInfo) return -1000; + + if (candidateMatchesPatterns(device, detectionSettings.blocklist)) return -1000; + if (candidateMatchesPatterns(device, detectionSettings.allowlist)) { + return 1000 + ((lcdInfo.screen_width || 0) * (lcdInfo.screen_height || 0)); + } + + var score = 0; + var type = String(device.d_type || device.type || ''); + var name = String(device.name || ''); + + if (type === 'Liquidctl') score += 30; + else if (type === 'Hwmon' || type === 'CustomSensors' || type === 'CPU' || type === 'GPU') score -= 40; + else if (type) score += 8; + + if (ciContains(name, 'kraken') || ciContains(name, 'elite') || ciContains(name, 'hydro') || + ciContains(name, 'capellix') || ciContains(name, 'lcd') || ciContains(name, 'display') || + ciContains(name, 'screen') || ciContains(name, 'aio')) { + score += 24; + } + + if (looksLikeMainboardCandidate(name)) score -= 80; + + if (lcdInfo.screen_width === lcdInfo.screen_height) score += 6; + else score += 3; + if (lcdInfo.screen_width >= 240 && lcdInfo.screen_height >= 240) score += 4; + + if (detectionSettings.mode === 'relaxed') return Math.max(score, 0); + if (detectionSettings.mode === 'balanced') return score >= 0 ? score : -1; + if (looksLikeMainboardCandidate(name)) return -1; + return score >= 20 ? score : -1; + } + + function findBestDetectedDevice(devices, config) { + var detectionSettings = getDetectionSettings(config); + var best = null; + var bestScore = -1001; + for (var i = 0; i < devices.length; i++) { + var score = scoreDetectedDevice(devices[i], detectionSettings); + if (score > bestScore) { + bestScore = score; + best = devices[i]; + } + } + return (bestScore >= 0) ? best : null; + } + // ===== SENSOR DISCOVERY ===== function setSensorDiscoveryStatus(type, html) { var el = document.getElementById('sensor-discovery-status'); @@ -1376,11 +1501,10 @@

System Environment

el.innerHTML = html; } - async function discoverSensors(apiAddress, token, password) { + async function discoverSensors(apiAddress, token) { // Keep global auth state in sync so that getDevices/getStatus fallbacks can use it lastApiAddress = apiAddress; lastApiToken = token; - lastApiPassword = password; try { // Use cc-plugin-lib functions (bypasses CORS sandbox in CC4 iframe). @@ -1487,14 +1611,11 @@

System Environment

function retrySensorDiscovery() { var apiAddr = lastApiAddress || 'http://localhost:11987'; var apiToken = lastApiToken || ''; - var apiPass = lastApiPassword || ''; // Also read current form values in case they were just changed var addrInput = document.querySelector('[name="daemon.address"]'); var tokenInput = document.querySelector('[name="daemon.access_token"]'); - var passInput = document.querySelector('[name="daemon.password"]'); if (addrInput && addrInput.value) apiAddr = addrInput.value; if (tokenInput) apiToken = tokenInput.value; - if (passInput) apiPass = passInput.value; var statusEl = document.getElementById('sensor-discovery-status'); if (statusEl) { statusEl.style.display = 'block'; @@ -1505,7 +1626,7 @@

System Environment

statusEl.style.fontSize = '13px'; statusEl.innerHTML = 'Discovering sensors…'; } - discoverSensors(apiAddr, apiToken, apiPass); + discoverSensors(apiAddr, apiToken); } function updateSensorSlotOptions() { @@ -1584,10 +1705,10 @@

System Environment

}; if (sensorId === 'liquid') { - defaults.threshold_1 = 25.0; - defaults.threshold_2 = 28.0; - defaults.threshold_3 = 31.0; - defaults.max_scale = 50.0; + defaults.threshold_1 = 30.0; + defaults.threshold_2 = 35.0; + defaults.threshold_3 = 40.0; + defaults.max_scale = 55.0; } else if (/RPM|rpm/.test(sensorId)) { defaults.threshold_1 = 500; defaults.threshold_2 = 1000; @@ -1739,17 +1860,17 @@

System Environment

'
' + '
' + '' + - 'Default: 100 (0 = global)' + + 'Default: auto (0 = dynamic/global)' + '' + '
' + '
' + '' + - 'Default: 0' + + 'Auto-layout fine-tuning only' + '' + '
' + '
' + '' + - 'Default: 0' + + 'Auto-layout fine-tuning only' + '' + '
' + '
' + @@ -1829,7 +1950,7 @@

System Environment

} function ensureSensorConfigsForSlots() { - var slotIds = ['sensor_slot_up', 'sensor_slot_mid', 'sensor_slot_down']; + var slotIds = ['sensor_slot_1', 'sensor_slot_2', 'sensor_slot_3']; var activeSensors = []; for (var i = 0; i < slotIds.length; i++) { @@ -1904,12 +2025,10 @@

System Environment

// ===== DEVICE DETECTION ===== var lastApiAddress = ''; var lastApiToken = ''; - var lastApiPassword = ''; - async function fetchDeviceInfo(apiAddress, token, password) { + async function fetchDeviceInfo(apiAddress, token) { lastApiAddress = apiAddress; lastApiToken = token; - lastApiPassword = password; var loadingEl = document.getElementById('device-loading'); var contentEl = document.getElementById('device-panel-content'); @@ -1924,18 +2043,7 @@

System Environment

var data = await getDevices(); var devices = data.devices || []; - var found = null; - for (var i = 0; i < devices.length; i++) { - var dev = devices[i]; - var dtype = dev.d_type || dev.type || ''; - if (dtype === 'Liquidctl') { - 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; - } - } - } + var found = findBestDetectedDevice(devices, buildConfig()); loadingEl.style.display = 'none'; @@ -1951,17 +2059,9 @@

System Environment

var liquidctlVer = (found.info && found.info.driver_info && found.info.driver_info.version) || '\u2014'; document.getElementById('device-liquidctl').textContent = liquidctlVer; - var width = 0, height = 0; - var channels = found.info && found.info.channels; - if (channels) { - 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; - } - } - } + var lcdInfo = getDeviceLcdInfo(found); + var width = lcdInfo ? (lcdInfo.screen_width || 0) : 0; + var height = lcdInfo ? (lcdInfo.screen_height || 0) : 0; var resEl = document.getElementById('device-resolution'); var shapeEl = document.getElementById('device-shape'); @@ -2001,18 +2101,17 @@

System Environment

} function refreshDeviceInfo() { - fetchDeviceInfo(lastApiAddress, lastApiToken, lastApiPassword); + fetchDeviceInfo(lastApiAddress, lastApiToken); } - async function fetchSystemInfo(apiAddress, token, password) { + async function fetchSystemInfo(apiAddress, token) { try { lastApiAddress = apiAddress; lastApiToken = token; - lastApiPassword = password; // /health has no cc-plugin-lib equivalent; only fetch it in dev/browser mode if (!isInCoolerControl) { - var headers = makeAuthHeaders(token, password); + var headers = makeAuthHeaders(token); var healthRes = await fetch(apiAddress + '/health', { headers: headers }); if (healthRes.ok) { var health = await healthRes.json(); @@ -2056,32 +2155,37 @@

System Environment

function toggleCircleInterval() { var mode = document.getElementById('display_mode').value; - document.getElementById('circle_interval_group').style.display = mode === 'circle' ? 'block' : 'none'; - 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'; + var isCircle = mode === 'circle'; + document.getElementById('circle_interval_group').style.display = isCircle ? 'block' : 'none'; + document.getElementById('circle_extra_info_group').style.display = isCircle ? 'block' : 'none'; + var midSlotGroup = document.getElementById('sensor_slot_2_group'); + var midBarHeightGroup = document.getElementById('bar_height_2_group'); + if (midSlotGroup) midSlotGroup.style.display = isCircle ? 'block' : 'none'; + if (midBarHeightGroup) midBarHeightGroup.style.display = isCircle ? 'block' : 'none'; if (mode === 'dual') { - var midSlot = document.getElementById('sensor_slot_mid'); + var midSlot = document.getElementById('sensor_slot_2'); if (midSlot) midSlot.value = 'none'; + } else if (mode === 'circle') { + var midSlot = document.getElementById('sensor_slot_2'); + if (midSlot && midSlot.value === 'none') midSlot.value = 'liquid'; } validateSensorSlots(); } function validateSensorSlots() { 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; + var slotUp = document.getElementById('sensor_slot_1').value; + var slotMid = document.getElementById('sensor_slot_2').value; + var slotDown = document.getElementById('sensor_slot_3').value; var warningDiv = document.getElementById('sensor_slot_warning'); var warningText = document.getElementById('sensor_slot_warning_text'); var warnings = []; 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 (slotUp !== 'none') activeSlots.push({ name: 'Slot 1', value: slotUp }); + if (mode === 'circle' && slotMid !== 'none') activeSlots.push({ name: 'Slot 2', value: slotMid }); + if (slotDown !== 'none') activeSlots.push({ name: 'Slot 3', value: slotDown }); if (activeSlots.length === 0) warnings.push('At least one sensor slot must be active.'); @@ -2139,8 +2243,24 @@

System Environment

} async function loadDefaultConfig() { - DEFAULT_CONFIG = FACTORY_DEFAULTS; - return DEFAULT_CONFIG; + var fallbackDefaults = cloneConfig(FACTORY_DEFAULTS); + + try { + var response = await fetch('../config.json', { cache: 'no-store' }); + if (!response.ok) { + throw new Error('HTTP ' + response.status); + } + + var parsedConfig = JSON.parse(await response.text()); + RESET_CONFIG = cloneConfig(parsedConfig); + DEFAULT_CONFIG = mergeConfigWithDefaults(parsedConfig, fallbackDefaults); + return DEFAULT_CONFIG; + } catch (error) { + console.warn('Falling back to embedded factory defaults:', error); + RESET_CONFIG = fallbackDefaults; + DEFAULT_CONFIG = cloneConfig(fallbackDefaults); + return DEFAULT_CONFIG; + } } // ===== BUILD CONFIG FROM FORM ===== @@ -2148,7 +2268,7 @@

System Environment

var form = document.getElementById('configForm'); var formData = new FormData(form); var config = { - daemon: {}, paths: {}, display: {}, layout: {}, colors: {}, + daemon: {}, device_detection: {}, paths: {}, display: {}, layout: {}, colors: {}, font: {}, sensors: {}, positioning: {} }; @@ -2160,7 +2280,7 @@

System Environment

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'); + var isSlotField = (parts[1] === 'sensor_slot_1' || parts[1] === 'sensor_slot_2' || parts[1] === 'sensor_slot_3'); if (isSlotField) { config[parts[0]][parts[1]] = value; } else { @@ -2182,10 +2302,11 @@

System Environment

// Sensor configs from dynamic form sections config.sensors = collectSensorConfigsFromDOM(); - // Convert tls_skip_verify to boolean for JSON compatibility with C daemon - if (config.daemon) config.daemon.tls_skip_verify = !!config.daemon.tls_skip_verify; + // Checkbox fields (not included in FormData when unchecked) + var extraInfoCb = document.getElementById('circle_show_extra_info'); + config.display.circle_show_extra_info = extraInfoCb ? extraInfoCb.checked : false; - return config; + return mergeConfigWithDefaults(config, DEFAULT_CONFIG || FACTORY_DEFAULTS); } // ===== POPULATE FORM FROM CONFIG ===== @@ -2208,9 +2329,15 @@

System Environment

} } + // Checkbox fields + var extraInfoCb = document.getElementById('circle_show_extra_info'); + if (extraInfoCb && config.display) { + extraInfoCb.checked = !!config.display.circle_show_extra_info; + } + // 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' }; + var slotFields = ['sensor_slot_1', 'sensor_slot_2', 'sensor_slot_3']; + var slotDefaults = { sensor_slot_1: 'cpu', sensor_slot_2: 'liquid', sensor_slot_3: 'gpu' }; for (var sf = 0; sf < slotFields.length; sf++) { var slotValue = config.display && config.display[slotFields[sf]]; if (slotValue) { @@ -2264,9 +2391,7 @@

System Environment

var config = buildConfig(); try { - await writeConfigToFile(config); - requestRestartAfterSave(1000); - await savePluginConfig(config); + await saveAndRestartConfig(config); alert("Configuration saved! Plugin will restart."); setTimeout(function() { window.close(); }, 300); } catch (error) { @@ -2295,85 +2420,31 @@

System Environment

} } - async function restartPluginDaemon(config) { - // CC4 restarts cc-plugin-coolerdash.service automatically when the plugin - // config is updated via PUT /plugins/{id}/config (text/plain as per CC4 API spec). - // This function triggers that restart by re-uploading the current config. - try { - var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987'; - var token = (config.daemon && config.daemon.access_token) || ''; - var password = (config.daemon && config.daemon.password) || ''; - var authHdr = makeAuthHeaders(token, password); - - var response = await fetch(apiAddress + '/plugins/coolerdash/config', { - method: 'PUT', - headers: Object.assign({ 'Content-Type': 'text/plain; charset=utf-8' }, authHdr), - body: JSON.stringify(config, null, 2) - }); - if (!response.ok) console.warn('Could not trigger plugin restart via config update: HTTP ' + response.status); - } catch (error) { - console.error('Plugin restart trigger failed:', error); - } - } - - function requestRestartAfterSave(delayMs) { + async function triggerPluginRestart(delayMs) { delayMs = delayMs || 800; - var doRestart = function() { - try { - if (typeof restart === 'function') restart(); - else restartPluginDaemon(buildConfig()); - } catch (e) { console.warn('Restart failed:', e); } - }; - if (typeof successfulConfigSaveCallback === 'function') { - successfulConfigSaveCallback(function() { setTimeout(doRestart, delayMs); }); - } else { - var handler = function(event) { - if (event.data && event.data.type === 'configSaved') { - window.removeEventListener('message', handler); - setTimeout(doRestart, delayMs); - } - }; - window.addEventListener('message', handler); + if (delayMs > 0) { + await new Promise(function(resolve) { setTimeout(resolve, delayMs); }); } - } - async function writeConfigToFile(config) { - try { - var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987'; - var token = (config.daemon && config.daemon.access_token) || ''; - var password = (config.daemon && config.daemon.password) || ''; - var authHdr = makeAuthHeaders(token, password); - - if (token || password) { - // CC4 API spec: PUT /plugins/{id}/config expects text/plain - var response = await fetch(apiAddress + '/plugins/coolerdash/config', { - method: 'PUT', - headers: Object.assign({ 'Content-Type': 'text/plain; charset=utf-8' }, authHdr), - body: JSON.stringify(config, null, 2) - }); - if (response.ok) return true; - } - - 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(); - URL.revokeObjectURL(a.href); - return false; - } catch (error) { - console.warn("Config write failed:", error); - return false; + if (typeof restart !== 'function') { + throw new Error('CoolerControl native restart API is unavailable in this context.'); } + + restart(); + return true; + } + + async function saveAndRestartConfig(config, delayMs) { + await savePluginConfig(config); + return triggerPluginRestart(delayMs); } async function resetConfig() { try { - await writeConfigToFile(FACTORY_DEFAULTS); - populateForm(FACTORY_DEFAULTS); - requestRestartAfterSave(1000); - await savePluginConfig(FACTORY_DEFAULTS); + var factoryConfig = cloneConfig(RESET_CONFIG || DEFAULT_CONFIG || FACTORY_DEFAULTS); + await saveAndRestartConfig(factoryConfig); + populateForm(factoryConfig); alert('Reset to factory defaults! Plugin will restart.'); setTimeout(function() { window.close(); }, 300); } catch (error) { @@ -2383,7 +2454,8 @@

System Environment

} // ===== LEGACY CONFIG MIGRATION ===== - function migrateConfig(config) { + function mergeConfigWithDefaults(config, defaultConfig) { + var defaults = defaultConfig || FACTORY_DEFAULTS; var sensors = config.sensors || {}; var needsMigration = false; @@ -2413,14 +2485,15 @@

System Environment

// 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 || {}) + daemon: Object.assign({}, defaults.daemon || {}, config.daemon || {}), + device_detection: Object.assign({}, defaults.device_detection || {}, config.device_detection || {}), + paths: Object.assign({}, defaults.paths || {}, config.paths || {}), + display: Object.assign({}, defaults.display || {}, config.display || {}), + layout: Object.assign({}, defaults.layout || {}, config.layout || {}), + colors: Object.assign({}, defaults.colors || {}, config.colors || {}), + font: Object.assign({}, defaults.font || {}, config.font || {}), + sensors: Object.assign({}, defaults.sensors || {}), + positioning: Object.assign({}, defaults.positioning || {}, config.positioning || {}) }; // Deep merge sensor configs (skip corrupt numeric keys) @@ -2440,7 +2513,7 @@

System Environment

} // Sanitize corrupt numeric slot values (from old parseFloat bug) - var slotDefaults = { sensor_slot_up: 'cpu', sensor_slot_mid: 'liquid', sensor_slot_down: 'gpu' }; + var slotDefaults = { sensor_slot_1: 'cpu', sensor_slot_2: 'liquid', sensor_slot_3: 'gpu' }; for (var slotKey in slotDefaults) { if (merged.display && merged.display[slotKey] !== undefined) { var sv = String(merged.display[slotKey]); @@ -2468,6 +2541,10 @@

System Environment

return merged; } + function migrateConfig(config) { + return mergeConfigWithDefaults(config, DEFAULT_CONFIG || FACTORY_DEFAULTS); + } + // ===== INITIALIZATION ===== runPluginScript(async function() { try { @@ -2485,10 +2562,9 @@

System Environment

// Detect device and discover sensors from CoolerControl API var apiAddr = (config.daemon && config.daemon.address) || 'http://localhost:11987'; var apiToken = (config.daemon && config.daemon.access_token) || ''; - var apiPass = (config.daemon && config.daemon.password) || ''; - fetchDeviceInfo(apiAddr, apiToken, apiPass); - fetchSystemInfo(apiAddr, apiToken, apiPass); - discoverSensors(apiAddr, apiToken, apiPass); + fetchDeviceInfo(apiAddr, apiToken); + fetchSystemInfo(apiAddr, apiToken); + discoverSensors(apiAddr, apiToken); } 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 2211d38..a669655 100644 --- a/src/device/config.c +++ b/src/device/config.c @@ -25,6 +25,9 @@ #include "config.h" #include "../srv/cc_conf.h" +#define CONFIG_LAYOUT_U16_UNSET UINT16_MAX +#define CONFIG_LAYOUT_U8_UNSET UINT8_MAX + // ============================================================================ // Global Logging Implementation // ============================================================================ @@ -70,12 +73,6 @@ static void set_daemon_defaults(Config *config) { SAFE_STRCPY(config->daemon_address, "http://localhost:11987"); } - if (config->daemon_password[0] == '\0') - { - SAFE_STRCPY(config->daemon_password, ""); - } - /* access_token, tls_ca_cert_path default to empty string; tls_skip_verify defaults to 0 */ - /* (already zeroed by memset in load_plugin_config) */ } /** @@ -102,6 +99,28 @@ static void set_paths_defaults(Config *config) } } +/** + * @brief Set LCD device detection defaults. + */ +static void set_device_detection_defaults(Config *config) +{ + if (config->device_detection_mode[0] == '\0') + { + SAFE_STRCPY(config->device_detection_mode, "strict"); + } + + if (config->device_detection_allowlist[0] == '\0') + { + SAFE_STRCPY(config->device_detection_allowlist, ""); + } + + if (config->device_detection_blocklist[0] == '\0') + { + SAFE_STRCPY(config->device_detection_blocklist, + "mainboard\nmotherboard\nchipset"); + } +} + /** * @brief Try to set display dimensions from LCD device. */ @@ -137,30 +156,28 @@ static void set_display_defaults(Config *config) config->lcd_brightness = 80; if (!is_valid_orientation(config->lcd_orientation)) config->lcd_orientation = 0; - if (config->display_shape[0] == '\0') - cc_safe_strcpy(config->display_shape, sizeof(config->display_shape), "auto"); if (config->display_mode[0] == '\0') - cc_safe_strcpy(config->display_mode, sizeof(config->display_mode), "dual"); + cc_safe_strcpy(config->display_mode, sizeof(config->display_mode), "circle"); if (config->display_background_image_fit[0] == '\0') cc_safe_strcpy(config->display_background_image_fit, sizeof(config->display_background_image_fit), "cover"); if (config->circle_switch_interval == 0) config->circle_switch_interval = 5; + if (config->circle_show_extra_info < 0) + config->circle_show_extra_info = 1; // enabled by default if (config->display_content_scale_factor == 0.0f) config->display_content_scale_factor = 0.98f; - if (config->display_inscribe_factor < 0.0f) - config->display_inscribe_factor = 0.70710678f; if (config->display_background_overlay_opacity < 0.0f || config->display_background_overlay_opacity > 1.0f) config->display_background_overlay_opacity = 0.0f; // Sensor slot defaults (flexible sensor assignment) - if (config->sensor_slot_up[0] == '\0') - cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), "cpu"); - if (config->sensor_slot_mid[0] == '\0') - cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), "liquid"); - if (config->sensor_slot_down[0] == '\0') - cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), "gpu"); + if (config->sensor_slot_1[0] == '\0') + cc_safe_strcpy(config->sensor_slot_1, sizeof(config->sensor_slot_1), "cpu"); + if (config->sensor_slot_2[0] == '\0') + cc_safe_strcpy(config->sensor_slot_2, sizeof(config->sensor_slot_2), "liquid"); + if (config->sensor_slot_3[0] == '\0') + cc_safe_strcpy(config->sensor_slot_3, sizeof(config->sensor_slot_3), "gpu"); } /** @@ -168,15 +185,15 @@ static void set_display_defaults(Config *config) */ static void set_layout_defaults(Config *config) { - if (config->layout_bar_width == 0) + if (config->layout_bar_width == CONFIG_LAYOUT_U8_UNSET) config->layout_bar_width = 98; - if (config->layout_label_margin_left == 0) + if (config->layout_label_margin_left == CONFIG_LAYOUT_U8_UNSET) config->layout_label_margin_left = 1; - if (config->layout_label_margin_bar == 0) + if (config->layout_label_margin_bar == CONFIG_LAYOUT_U8_UNSET) config->layout_label_margin_bar = 1; - if (config->layout_bar_height == 0) + if (config->layout_bar_height == CONFIG_LAYOUT_U16_UNSET) config->layout_bar_height = 24; - if (config->layout_bar_gap == 0) + if (config->layout_bar_gap == CONFIG_LAYOUT_U16_UNSET) config->layout_bar_gap = 12.0f; // bar_border: -1 = use default (1.0), 0 = explicitly disabled, >0 = custom value if (config->layout_bar_border < 0.0f) @@ -187,13 +204,13 @@ static void set_layout_defaults(Config *config) if (config->layout_bar_border_enabled < 0) config->layout_bar_border_enabled = 1; // Default: enabled - // Individual bar heights per slot (default to main bar_height) - if (config->layout_bar_height_up == 0) - config->layout_bar_height_up = config->layout_bar_height; - if (config->layout_bar_height_mid == 0) - config->layout_bar_height_mid = config->layout_bar_height; - if (config->layout_bar_height_down == 0) - config->layout_bar_height_down = config->layout_bar_height; + // Individual bar heights default to inherit-from-global when unspecified. + if (config->layout_bar_height_1 == CONFIG_LAYOUT_U16_UNSET) + config->layout_bar_height_1 = 0; + if (config->layout_bar_height_2 == CONFIG_LAYOUT_U16_UNSET) + config->layout_bar_height_2 = 0; + if (config->layout_bar_height_3 == CONFIG_LAYOUT_U16_UNSET) + config->layout_bar_height_3 = 0; } /** @@ -204,7 +221,7 @@ static void set_display_positioning_defaults(Config *config) // 0 = automatic positioning (default behavior) // Note: All offset values default to 0, which means automatic positioning - if (config->display_degree_spacing == 0) + if (config->display_degree_spacing < 0) config->display_degree_spacing = 16; } @@ -217,38 +234,53 @@ static void set_font_defaults(Config *config) SAFE_STRCPY(config->font_face, "Roboto Black"); if (config->font_size_temp == 0.0f) - { - const double base_resolution = 240.0; - const double base_font_size_temp = 100.0; - const double scale_factor = - ((double)config->display_width + (double)config->display_height) / - (2.0 * base_resolution); - config->font_size_temp = (float)(base_font_size_temp * scale_factor); - - log_message( - LOG_INFO, - "Font size (temp) auto-scaled: %.1f (display: %dx%d, scale: %.2f)", - config->font_size_temp, config->display_width, config->display_height, - scale_factor); - } + log_message(LOG_INFO, + "Font size (temp): automatic renderer sizing enabled"); if (config->font_size_labels == 0.0f) + log_message(LOG_INFO, + "Font size (labels): automatic renderer sizing enabled"); + + if (config->font_growth_factor < 1.0f) + config->font_growth_factor = 1.33f; + + set_display_positioning_defaults(config); +} + +/** + * @brief Load LCD device detection settings from JSON. + */ +static void load_device_detection_from_json(json_t *root, Config *config) +{ + json_t *device_detection = json_object_get(root, "device_detection"); + if (!device_detection || !json_is_object(device_detection)) + return; + + json_t *mode = json_object_get(device_detection, "mode"); + if (mode && json_is_string(mode)) { - const double base_resolution = 240.0; - const double base_font_size_labels = 30.0; - const double scale_factor = - ((double)config->display_width + (double)config->display_height) / - (2.0 * base_resolution); - config->font_size_labels = (float)(base_font_size_labels * scale_factor); + const char *value = json_string_value(mode); + if (value && + (strcmp(value, "strict") == 0 || strcmp(value, "balanced") == 0 || + strcmp(value, "relaxed") == 0)) + { + SAFE_STRCPY(config->device_detection_mode, value); + } + } - log_message( - LOG_INFO, - "Font size (labels) auto-scaled: %.1f (display: %dx%d, scale: %.2f)", - config->font_size_labels, config->display_width, config->display_height, - scale_factor); + json_t *allowlist = json_object_get(device_detection, "allowlist"); + if (allowlist && json_is_string(allowlist)) + { + SAFE_STRCPY(config->device_detection_allowlist, + json_string_value(allowlist)); } - set_display_positioning_defaults(config); + json_t *blocklist = json_object_get(device_detection, "blocklist"); + if (blocklist && json_is_string(blocklist)) + { + SAFE_STRCPY(config->device_detection_blocklist, + json_string_value(blocklist)); + } } /** @@ -377,18 +409,18 @@ static void set_default_sensor_configs(Config *config) gpu->max_scale = 115.0f; } - /* Ensure Liquid config (lower thresholds) */ + /* Ensure Liquid config (AIO coolant thresholds) */ SensorConfig *liquid = ensure_sensor_config(config, "liquid"); if (liquid) { if (liquid->threshold_1 == 0.0f) - liquid->threshold_1 = 25.0f; + liquid->threshold_1 = 30.0f; if (liquid->threshold_2 == 0.0f) - liquid->threshold_2 = 28.0f; + liquid->threshold_2 = 35.0f; if (liquid->threshold_3 == 0.0f) - liquid->threshold_3 = 31.0f; + liquid->threshold_3 = 40.0f; if (liquid->max_scale == 0.0f) - liquid->max_scale = 50.0f; + liquid->max_scale = 55.0f; } } @@ -496,25 +528,25 @@ static void validate_sensor_slots(Config *config) int reset_needed = 0; // Validate slot values (must be cpu/gpu/liquid/none) - if (!is_valid_sensor_slot(config->sensor_slot_up)) + if (!is_valid_sensor_slot(config->sensor_slot_1)) { - log_message(LOG_WARNING, "Invalid sensor_slot_up value, using 'cpu'"); - cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), "cpu"); + log_message(LOG_WARNING, "Invalid sensor_slot_1 value, using 'cpu'"); + cc_safe_strcpy(config->sensor_slot_1, sizeof(config->sensor_slot_1), "cpu"); } - if (!is_valid_sensor_slot(config->sensor_slot_mid)) + if (!is_valid_sensor_slot(config->sensor_slot_2)) { - log_message(LOG_WARNING, "Invalid sensor_slot_mid value, using 'liquid'"); - cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), "liquid"); + log_message(LOG_WARNING, "Invalid sensor_slot_2 value, using 'liquid'"); + cc_safe_strcpy(config->sensor_slot_2, sizeof(config->sensor_slot_2), "liquid"); } - if (!is_valid_sensor_slot(config->sensor_slot_down)) + if (!is_valid_sensor_slot(config->sensor_slot_3)) { - log_message(LOG_WARNING, "Invalid sensor_slot_down value, using 'gpu'"); - cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), "gpu"); + log_message(LOG_WARNING, "Invalid sensor_slot_3 value, using 'gpu'"); + cc_safe_strcpy(config->sensor_slot_3, sizeof(config->sensor_slot_3), "gpu"); } // Check for duplicates (only among active slots, "none" can appear multiple times) - const char *slots[] = {config->sensor_slot_up, config->sensor_slot_mid, config->sensor_slot_down}; - const char *slot_names[] = {"sensor_slot_up", "sensor_slot_mid", "sensor_slot_down"}; + const char *slots[] = {config->sensor_slot_1, config->sensor_slot_2, config->sensor_slot_3}; + const char *slot_names[] = {"sensor_slot_1", "sensor_slot_2", "sensor_slot_3"}; for (int i = 0; i < 3; i++) { @@ -539,9 +571,9 @@ static void validate_sensor_slots(Config *config) } // Check that at least one slot is active - if (!slot_is_active_str(config->sensor_slot_up) && - !slot_is_active_str(config->sensor_slot_mid) && - !slot_is_active_str(config->sensor_slot_down)) + if (!slot_is_active_str(config->sensor_slot_1) && + !slot_is_active_str(config->sensor_slot_2) && + !slot_is_active_str(config->sensor_slot_3)) { log_message(LOG_ERROR, "All sensor slots are 'none'. At least one sensor must be active. Resetting to defaults."); reset_needed = 1; @@ -550,10 +582,10 @@ static void validate_sensor_slots(Config *config) // Reset to defaults if validation failed if (reset_needed) { - cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), "cpu"); - cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), "liquid"); - cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), "gpu"); - log_message(LOG_STATUS, "Sensor slots reset to defaults: up=cpu, mid=liquid, down=gpu"); + cc_safe_strcpy(config->sensor_slot_1, sizeof(config->sensor_slot_1), "cpu"); + cc_safe_strcpy(config->sensor_slot_2, sizeof(config->sensor_slot_2), "liquid"); + cc_safe_strcpy(config->sensor_slot_3, sizeof(config->sensor_slot_3), "gpu"); + log_message(LOG_STATUS, "Sensor slots reset to defaults: 1=cpu, 2=liquid, 3=gpu"); } } @@ -567,6 +599,7 @@ static void apply_system_defaults(Config *config) set_daemon_defaults(config); set_paths_defaults(config); + set_device_detection_defaults(config); set_display_defaults(config); set_layout_defaults(config); set_font_defaults(config); @@ -660,16 +693,6 @@ static void load_daemon_from_json(json_t *root, Config *config) } } - json_t *password = json_object_get(daemon, "password"); - if (password && json_is_string(password) && json_string_length(password) > 0) - { - const char *value = json_string_value(password); - if (value) - { - SAFE_STRCPY(config->daemon_password, value); - } - } - json_t *token = json_object_get(daemon, "access_token"); if (token && json_is_string(token) && json_string_length(token) > 0) { @@ -679,22 +702,6 @@ static void load_daemon_from_json(json_t *root, Config *config) SAFE_STRCPY(config->access_token, value); } } - - json_t *ca_cert = json_object_get(daemon, "tls_ca_cert_path"); - if (ca_cert && json_is_string(ca_cert) && json_string_length(ca_cert) > 0) - { - const char *value = json_string_value(ca_cert); - if (value) - { - SAFE_STRCPY(config->tls_ca_cert_path, value); - } - } - - json_t *skip_verify = json_object_get(daemon, "tls_skip_verify"); - if (skip_verify && json_is_boolean(skip_verify)) - { - config->tls_skip_verify = json_is_true(skip_verify) ? 1 : 0; - } } /** @@ -774,6 +781,15 @@ static void load_display_from_json(json_t *root, Config *config) config->circle_switch_interval = (uint16_t)val; } + json_t *extra_info = json_object_get(display, "circle_show_extra_info"); + if (extra_info) + { + if (json_is_boolean(extra_info)) + config->circle_show_extra_info = json_is_true(extra_info) ? 1 : 0; + else if (json_is_integer(extra_info)) + config->circle_show_extra_info = json_integer_value(extra_info) ? 1 : 0; + } + json_t *background_fit = json_object_get(display, "background_image_fit"); if (background_fit && json_is_string(background_fit)) { @@ -835,12 +851,6 @@ static void load_display_from_json(json_t *root, Config *config) config->display_height = (uint16_t)val; } - json_t *shape = json_object_get(display, "shape"); - if (shape && json_is_string(shape)) - { - SAFE_STRCPY(config->display_shape, json_string_value(shape)); - } - json_t *scale_factor = json_object_get(display, "content_scale_factor"); if (scale_factor && json_is_number(scale_factor)) { @@ -849,52 +859,44 @@ static void load_display_from_json(json_t *root, Config *config) config->display_content_scale_factor = (float)val; } - json_t *inscribe = json_object_get(display, "inscribe_factor"); - if (inscribe && json_is_number(inscribe)) - { - double val = json_number_value(inscribe); - if (val >= 0.0 && val <= 1.0) - config->display_inscribe_factor = (float)val; - } - // Sensor slot configuration - json_t *slot_up = json_object_get(display, "sensor_slot_up"); - if (slot_up && json_is_string(slot_up)) + json_t *slot_1 = json_object_get(display, "sensor_slot_1"); + if (slot_1 && json_is_string(slot_1)) { - const char *value = json_string_value(slot_up); + const char *value = json_string_value(slot_1); if (value) - cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), value); + cc_safe_strcpy(config->sensor_slot_1, sizeof(config->sensor_slot_1), value); } - else if (slot_up && !json_is_string(slot_up)) + else if (slot_1 && !json_is_string(slot_1)) { - log_message(LOG_WARNING, "sensor_slot_up has non-string type, using default '%s'", - config->sensor_slot_up); + log_message(LOG_WARNING, "sensor_slot_1 has non-string type, using default '%s'", + config->sensor_slot_1); } - json_t *slot_mid = json_object_get(display, "sensor_slot_mid"); - if (slot_mid && json_is_string(slot_mid)) + json_t *slot_2 = json_object_get(display, "sensor_slot_2"); + if (slot_2 && json_is_string(slot_2)) { - const char *value = json_string_value(slot_mid); + const char *value = json_string_value(slot_2); if (value) - cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), value); + cc_safe_strcpy(config->sensor_slot_2, sizeof(config->sensor_slot_2), value); } - else if (slot_mid && !json_is_string(slot_mid)) + else if (slot_2 && !json_is_string(slot_2)) { - log_message(LOG_WARNING, "sensor_slot_mid has non-string type, using default '%s'", - config->sensor_slot_mid); + log_message(LOG_WARNING, "sensor_slot_2 has non-string type, using default '%s'", + config->sensor_slot_2); } - json_t *slot_down = json_object_get(display, "sensor_slot_down"); - if (slot_down && json_is_string(slot_down)) + json_t *slot_3 = json_object_get(display, "sensor_slot_3"); + if (slot_3 && json_is_string(slot_3)) { - const char *value = json_string_value(slot_down); + const char *value = json_string_value(slot_3); if (value) - cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), value); + cc_safe_strcpy(config->sensor_slot_3, sizeof(config->sensor_slot_3), value); } - else if (slot_down && !json_is_string(slot_down)) + else if (slot_3 && !json_is_string(slot_3)) { - log_message(LOG_WARNING, "sensor_slot_down has non-string type, using default '%s'", - config->sensor_slot_down); + log_message(LOG_WARNING, "sensor_slot_3 has non-string type, using default '%s'", + config->sensor_slot_3); } } @@ -961,7 +963,7 @@ static void load_layout_from_json(json_t *root, Config *config) if (label_margin_left && json_is_integer(label_margin_left)) { int val = (int)json_integer_value(label_margin_left); - if (val >= 1 && val <= 50) + if (val >= 0 && val <= 50) config->layout_label_margin_left = (uint8_t)val; } @@ -969,33 +971,33 @@ static void load_layout_from_json(json_t *root, Config *config) if (label_margin_bar && json_is_integer(label_margin_bar)) { int val = (int)json_integer_value(label_margin_bar); - if (val >= 1 && val <= 20) + if (val >= 0 && val <= 20) config->layout_label_margin_bar = (uint8_t)val; } // Individual bar heights per slot - json_t *bar_height_up = json_object_get(layout, "bar_height_up"); - if (bar_height_up && json_is_integer(bar_height_up)) + json_t *bar_height_1 = json_object_get(layout, "bar_height_1"); + if (bar_height_1 && json_is_integer(bar_height_1)) { - int val = (int)json_integer_value(bar_height_up); - if (val > 0 && val <= 100) - config->layout_bar_height_up = (uint16_t)val; + int val = (int)json_integer_value(bar_height_1); + if (val >= 0 && val <= 100) + config->layout_bar_height_1 = (uint16_t)val; } - json_t *bar_height_mid = json_object_get(layout, "bar_height_mid"); - if (bar_height_mid && json_is_integer(bar_height_mid)) + json_t *bar_height_2 = json_object_get(layout, "bar_height_2"); + if (bar_height_2 && json_is_integer(bar_height_2)) { - int val = (int)json_integer_value(bar_height_mid); - if (val > 0 && val <= 100) - config->layout_bar_height_mid = (uint16_t)val; + int val = (int)json_integer_value(bar_height_2); + if (val >= 0 && val <= 100) + config->layout_bar_height_2 = (uint16_t)val; } - json_t *bar_height_down = json_object_get(layout, "bar_height_down"); - if (bar_height_down && json_is_integer(bar_height_down)) + json_t *bar_height_3 = json_object_get(layout, "bar_height_3"); + if (bar_height_3 && json_is_integer(bar_height_3)) { - int val = (int)json_integer_value(bar_height_down); - if (val > 0 && val <= 100) - config->layout_bar_height_down = (uint16_t)val; + int val = (int)json_integer_value(bar_height_3); + if (val >= 0 && val <= 100) + config->layout_bar_height_3 = (uint16_t)val; } } @@ -1045,6 +1047,14 @@ static void load_font_from_json(json_t *root, Config *config) if (val >= 5.0 && val <= 100.0) config->font_size_labels = (float)val; } + + json_t *growth = json_object_get(font, "font_growth_factor"); + if (growth && json_is_number(growth)) + { + double val = json_number_value(growth); + if (val >= 1.0) + config->font_growth_factor = (float)val; + } } /** @@ -1220,10 +1230,19 @@ int load_plugin_config(Config *config, const char *config_path) // Initialize with defaults (memset sets all to 0, including color.is_set = 0) memset(config, 0, sizeof(Config)); - config->display_inscribe_factor = -1.0f; // Sentinel for "auto" - config->layout_bar_border = -1.0f; // Sentinel for "use default" - config->layout_bar_opacity = -1.0f; // Sentinel for "use default" - config->layout_bar_border_enabled = -1; // Sentinel for "auto" (enabled) + config->layout_bar_height = CONFIG_LAYOUT_U16_UNSET; + config->layout_bar_height_1 = CONFIG_LAYOUT_U16_UNSET; + config->layout_bar_height_2 = CONFIG_LAYOUT_U16_UNSET; + config->layout_bar_height_3 = CONFIG_LAYOUT_U16_UNSET; + config->layout_bar_gap = CONFIG_LAYOUT_U16_UNSET; + config->layout_bar_width = CONFIG_LAYOUT_U8_UNSET; + config->layout_label_margin_left = CONFIG_LAYOUT_U8_UNSET; + config->layout_label_margin_bar = CONFIG_LAYOUT_U8_UNSET; + config->layout_bar_border = -1.0f; // Sentinel for "use default" + config->layout_bar_opacity = -1.0f; // Sentinel for "use default" + config->layout_bar_border_enabled = -1; // Sentinel for "auto" (enabled) + config->circle_show_extra_info = -1; // Sentinel for "auto" (enabled) + config->display_degree_spacing = -1; // Note: All colors have is_set=0 after memset, so defaults will be applied // Try to find and load JSON config @@ -1241,6 +1260,7 @@ int load_plugin_config(Config *config, const char *config_path) { load_daemon_from_json(root, config); load_paths_from_json(root, config); + load_device_detection_from_json(root, config); load_display_from_json(root, config); load_layout_from_json(root, config); load_colors_from_json(root, config); diff --git a/src/device/config.h b/src/device/config.h index fc9704c..476ce58 100644 --- a/src/device/config.h +++ b/src/device/config.h @@ -22,11 +22,11 @@ // Configuration constants #define CONFIG_MAX_STRING_LEN 256 -#define CONFIG_MAX_PASSWORD_LEN 128 #define CONFIG_MAX_TOKEN_LEN 64 #define CONFIG_MAX_PATH_LEN 512 #define CONFIG_MAX_FONT_NAME_LEN 64 #define CONFIG_MAX_SENSOR_SLOT_LEN 256 +#define CONFIG_MAX_PATTERN_LIST_LEN 512 /** * @brief Simple color structure. @@ -93,10 +93,7 @@ typedef struct Config { // Daemon configuration char daemon_address[CONFIG_MAX_STRING_LEN]; - char daemon_password[CONFIG_MAX_PASSWORD_LEN]; char access_token[CONFIG_MAX_TOKEN_LEN]; - char tls_ca_cert_path[CONFIG_MAX_PATH_LEN]; - int tls_skip_verify; // Paths configuration char paths_images[CONFIG_MAX_PATH_LEN]; @@ -104,31 +101,34 @@ typedef struct Config char paths_image_background[CONFIG_MAX_PATH_LEN]; char paths_image_shutdown[CONFIG_MAX_PATH_LEN]; + // LCD device detection configuration + char device_detection_mode[16]; + char device_detection_allowlist[CONFIG_MAX_PATTERN_LIST_LEN]; + char device_detection_blocklist[CONFIG_MAX_PATTERN_LIST_LEN]; + // Display configuration uint16_t display_width; uint16_t display_height; float display_refresh_interval; uint8_t lcd_brightness; uint8_t lcd_orientation; - int force_display_circular; - char display_shape[16]; char display_mode[16]; char display_background_image_fit[16]; uint16_t circle_switch_interval; + int circle_show_extra_info; float display_content_scale_factor; - float display_inscribe_factor; float display_background_overlay_opacity; // Sensor slot configuration (flexible sensor assignment) - char sensor_slot_up[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" - char sensor_slot_mid[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" - char sensor_slot_down[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" + char sensor_slot_1[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" + char sensor_slot_2[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" + char sensor_slot_3[CONFIG_MAX_SENSOR_SLOT_LEN]; // "cpu", "gpu", "liquid", "none" // Layout configuration uint16_t layout_bar_height; - uint16_t layout_bar_height_up; // Individual bar height for upper slot - uint16_t layout_bar_height_mid; // Individual bar height for middle slot - uint16_t layout_bar_height_down; // Individual bar height for lower slot + uint16_t layout_bar_height_1; // Individual bar height for slot 1 + uint16_t layout_bar_height_2; // Individual bar height for slot 2 + uint16_t layout_bar_height_3; // Individual bar height for slot 3 uint16_t layout_bar_gap; float layout_bar_border; float layout_bar_opacity; @@ -144,6 +144,7 @@ typedef struct Config char font_face[CONFIG_MAX_FONT_NAME_LEN]; float font_size_temp; float font_size_labels; + float font_growth_factor; Color font_color_temp; Color font_color_label; diff --git a/src/main.c b/src/main.c index fb3e35a..f4fdae0 100644 --- a/src/main.c +++ b/src/main.c @@ -53,9 +53,6 @@ static volatile sig_atomic_t running = 1; // flag whether daemon is running * parameter). */ int verbose_logging = 0; // Only ERROR and WARNING by default (exported) -// Developer/testing flag: when set (via --develop), force displays to be -// treated as circular -int force_display_circular = 0; /** * @brief Global pointer to configuration. @@ -199,8 +196,7 @@ static void show_help(const char *program_name) " --dual Force dual display mode (CPU+GPU simultaneously)\n"); printf(" --circle Force circle mode (alternating CPU/GPU every 2.5 " "seconds)\n"); - printf(" --develop Developer: force display to be treated as " - "circular for testing\n\n"); + printf(" --develop Developer mode: enable verbose logging\n\n"); printf("DISPLAY MODES:\n"); printf( " dual Default mode - shows CPU and GPU simultaneously\n"); @@ -467,7 +463,6 @@ static const char *parse_arguments(int argc, char **argv, } else if (strcmp(argv[i], "--develop") == 0) { - force_display_circular = 1; verbose_logging = 1; // Developer mode implies verbose logging } else if (argv[i][0] != '-') @@ -525,14 +520,6 @@ static int initialize_config_and_instance(const char *config_path, log_message(LOG_INFO, "Using hardcoded defaults (no config.json found)"); } - /* Apply CLI overrides (developer/testing) */ - if (force_display_circular) - { - config->force_display_circular = 1; - log_message(LOG_INFO, "Developer override: forcing circular display " - "detection (via --develop)"); - } - int is_plugin_mode = is_started_as_plugin(); log_message(LOG_INFO, "Running mode: %s", is_plugin_mode ? "CoolerControl plugin" : "standalone"); @@ -561,7 +548,7 @@ static int initialize_coolercontrol_services(const Config *config) "Please check:\n" " - Is coolercontrold running? (systemctl status coolercontrold)\n" " - Is the daemon running on %s?\n" - " - Is the password correct in configuration?\n" + " - Is a valid access token configured?\n" " - Are network connections allowed?\n", config->daemon_address); fflush(stderr); @@ -576,7 +563,7 @@ static int initialize_coolercontrol_services(const Config *config) "Please check:\n" " - Is coolercontrold running? (systemctl status coolercontrold)\n" " - Is the daemon running on %s?\n" - " - Is the password correct in configuration?\n" + " - Is a valid access token configured?\n" " - Are network connections allowed?\n", config->daemon_address); return -1; diff --git a/src/mods/circle.c b/src/mods/circle.c index 24ab737..668a498 100644 --- a/src/mods/circle.c +++ b/src/mods/circle.c @@ -32,13 +32,197 @@ /** * @brief Global state for sensor alternation (slot-based cycling). */ -static int current_slot_index = 0; // 0=up, 1=mid, 2=down +static int current_slot_index = 0; // 0=slot1, 1=slot2, 2=slot3 static time_t last_switch_time = 0; +/** + * @brief Find pump RPM sensor for a Liquidctl device. + * @details Searches for an RPM sensor whose name contains "pump" + * (case-insensitive). Falls back to first RPM sensor if no pump found. + * @param data Sensor data collection + * @return Pointer to pump RPM sensor, or NULL if not found + */ +static const sensor_entry_t *find_liquid_pump_rpm( + const monitor_sensor_data_t *data) +{ + if (!data) + return NULL; + + const sensor_entry_t *first_rpm = NULL; + for (int i = 0; i < data->sensor_count; i++) + { + if (data->sensors[i].category != SENSOR_CATEGORY_RPM) + continue; + if (strcmp(data->sensors[i].device_type, "Liquidctl") != 0) + continue; + + /* Prefer sensor with "pump" in the name */ + if (strstr(data->sensors[i].name, "pump")) + return &data->sensors[i]; + + if (!first_rpm) + first_rpm = &data->sensors[i]; + } + + return first_rpm; +} + +/** + * @brief Build extra info text (freq/watts/RPM) for a slot. + * @details Maps slot type to channel sensors: + * CPU → freq + watts ("X.X GHz XXW") + * GPU → freq + watts ("XXXX MHz XXW") + * Liquid → RPM ("XXXX RPM") + * Dynamic → tries freq, then watts, then RPM + * @param data Sensor data collection + * @param slot_value Slot configuration value + * @param buf Output buffer + * @param buf_size Output buffer size + * @return 1 if text was written, 0 if no data available + */ +static int get_extra_info_text(const monitor_sensor_data_t *data, + const char *slot_value, + char *buf, size_t buf_size) +{ + if (!data || !slot_value || !buf || buf_size == 0) + return 0; + + buf[0] = '\0'; + + if (strcmp(slot_value, "cpu") == 0) + { + const sensor_entry_t *freq = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_FREQ); + const sensor_entry_t *watts = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_WATTS); + if (!freq && !watts) + return 0; + if (freq && watts) + { + if (freq->value >= 1000.0f) + snprintf(buf, buf_size, "%.1f GHz %.0fW", + freq->value / 1000.0f, watts->value); + else + snprintf(buf, buf_size, "%.0f MHz %.0fW", + freq->value, watts->value); + } + else if (freq) + { + if (freq->value >= 1000.0f) + snprintf(buf, buf_size, "%.1f GHz", freq->value / 1000.0f); + else + snprintf(buf, buf_size, "%.0f MHz", freq->value); + } + else + snprintf(buf, buf_size, "%.0fW", watts->value); + return 1; + } + + if (strcmp(slot_value, "gpu") == 0) + { + const sensor_entry_t *freq = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_FREQ); + const sensor_entry_t *watts = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_WATTS); + if (!freq && !watts) + return 0; + if (freq && watts) + snprintf(buf, buf_size, "%.0f MHz %.0fW", + freq->value, watts->value); + else if (freq) + snprintf(buf, buf_size, "%.0f MHz", freq->value); + else + snprintf(buf, buf_size, "%.0fW", watts->value); + return 1; + } + + if (strcmp(slot_value, "liquid") == 0) + { + const sensor_entry_t *rpm = find_liquid_pump_rpm(data); + if (!rpm) + return 0; + snprintf(buf, buf_size, "%.0f RPM", rpm->value); + return 1; + } + + /* Dynamic slot: try freq → watts → rpm */ + const sensor_entry_t *s = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_FREQ); + if (s) + { + if (s->value >= 1000.0f) + snprintf(buf, buf_size, "%.1f GHz", s->value / 1000.0f); + else + snprintf(buf, buf_size, "%.0f MHz", s->value); + return 1; + } + + s = find_channel_sensor_for_slot(data, slot_value, SENSOR_CATEGORY_WATTS); + if (s) + { + snprintf(buf, buf_size, "%.0fW", s->value); + return 1; + } + + s = find_channel_sensor_for_slot(data, slot_value, SENSOR_CATEGORY_RPM); + if (s) + { + snprintf(buf, buf_size, "%.0f RPM", s->value); + return 1; + } + + return 0; +} + +/** + * @brief Build second line of extra info (fan RPM for CPU/GPU). + * @details CPU fan RPM comes from the Liquidctl device (AIO cooler), + * since the CPU device itself has no fan sensor. GPU fan RPM comes + * from the GPU device directly. + * @param data Sensor data collection + * @param slot_value Slot configuration value + * @param buf Output buffer + * @param buf_size Output buffer size + * @return 1 if text was written, 0 if no data available + */ +static int get_extra_info_line2(const monitor_sensor_data_t *data, + const char *slot_value, + char *buf, size_t buf_size) +{ + if (!data || !slot_value || !buf || buf_size == 0) + return 0; + + buf[0] = '\0'; + + /* CPU: fan RPM comes from Liquidctl (AIO cooler fan, not pump) */ + if (strcmp(slot_value, "cpu") == 0) + { + const sensor_entry_t *rpm = find_channel_sensor_for_slot( + data, "liquid", SENSOR_CATEGORY_RPM); + if (!rpm) + return 0; + snprintf(buf, buf_size, "%.0f RPM", rpm->value); + return 1; + } + + /* GPU: fan RPM from the GPU device itself */ + if (strcmp(slot_value, "gpu") == 0) + { + const sensor_entry_t *rpm = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_RPM); + if (!rpm) + return 0; + snprintf(buf, buf_size, "%.0f RPM", rpm->value); + return 1; + } + + return 0; +} + /** * @brief Get the slot value for a given slot index. * @param config Configuration - * @param slot_index 0=up, 1=mid, 2=down + * @param slot_index 0=slot1, 1=slot2, 2=slot3 * @return Slot value string ("cpu", "gpu", "liquid", "none") */ static const char *get_slot_value_by_index(const struct Config *config, int slot_index) @@ -49,11 +233,11 @@ static const char *get_slot_value_by_index(const struct Config *config, int slot switch (slot_index) { case 0: - return config->sensor_slot_up; + return config->sensor_slot_1; case 1: - return config->sensor_slot_mid; + return config->sensor_slot_2; case 2: - return config->sensor_slot_down; + return config->sensor_slot_3; default: return "none"; } @@ -67,13 +251,13 @@ static const char *get_slot_name_by_index(int slot_index) switch (slot_index) { case 0: - return "up"; + return "1"; case 1: - return "mid"; + return "2"; case 2: - return "down"; + return "3"; default: - return "up"; + return "1"; } } @@ -165,79 +349,81 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config, const float max_temp = get_slot_max_scale(config, slot_value); const int effective_bar_width = params->safe_bar_width; + const int bar_height = get_scaled_slot_bar_height( + config, params, get_slot_name_by_index(current_slot_index)); + const int bar_x = (int)lround(params->safe_content_margin); - // Get bar height for current slot (use "up" slot as reference since circle shows one at a time) - const int bar_height = get_slot_bar_height(config, get_slot_name_by_index(current_slot_index)); + const double region_gap = get_effective_label_spacing(config, params); + const double label_padding = fmax(2.0, scale_value_avg(params, 4.0)); - // Calculate vertical layout - BAR is centered - const int bar_y = (config->display_height - bar_height) / 2; - const int bar_x = (config->display_width - effective_bar_width) / 2; - const int bar_right = bar_x + effective_bar_width; + double label_band_height = 0.0; + double label_font_size = get_preferred_label_font_size(config, params); + cairo_font_extents_t label_font_ext = {0}; + cairo_text_extents_t label_text_ext = {0}; - // Temperature above the bar (10% of display height above bar) - const int temp_spacing = (int)(config->display_height * 0.10); - const int temp_y = bar_y - temp_spacing; - - // Draw temperature value (centered horizontally INCLUDING degree symbol) - char temp_str[16]; + if (label_text) + { + const double left_margin_factor = + (config->layout_label_margin_left > 0) + ? (config->layout_label_margin_left / 100.0) + : 0.01; + const double label_left_padding = effective_bar_width * left_margin_factor; + const double available_label_width = + fmax(24.0, effective_bar_width - label_left_padding); + const double min_label_font_size = + (config->font_size_labels > 0.0f) + ? fmax(12.0, scale_value_avg(params, + (double)config->font_size_labels) * + 0.70) + : fmax(12.0, scale_value_avg(params, 12.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); + cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_NORMAL); + while (1) + { + cairo_set_font_size(cr, label_font_size); + cairo_font_extents(cr, &label_font_ext); + cairo_text_extents(cr, label_text, &label_text_ext); + if (fmax(label_text_ext.x_advance, label_text_ext.width) <= + available_label_width || + label_font_size <= min_label_font_size) + break; + + label_font_size *= 0.94; + if (label_font_size < min_label_font_size) + label_font_size = min_label_font_size; + } - const Color *value_color = &config->font_color_temp; + label_band_height = label_font_ext.ascent + label_font_ext.descent + + (2.0 * label_padding); + } - // Use per-slot font size - float slot_font_size = get_slot_font_size(config, slot_value); + const double grouped_height = + bar_height + (label_text ? (region_gap + label_band_height) : 0.0); + const int bar_y = + (int)lround(fmax(0.0, (config->display_height - grouped_height) / 2.0)); + const double value_bar_gap = region_gap * 0.05; - cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_BOLD); - cairo_set_font_size(cr, slot_font_size); - set_cairo_color(cr, value_color); + const double value_box_y = 0.0; + const double value_box_height = fmax(0.0, bar_y - value_bar_gap); + const double label_box_y = bar_y + bar_height + region_gap; + const double label_box_height = + fmax(0.0, config->display_height - label_box_y); - 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, slot_font_size / 1.66); - cairo_text_extents_t degree_ext; - cairo_text_extents(cr, "°", °ree_ext); - cairo_set_font_size(cr, slot_font_size); - - // Right-align temperature + degree symbol block to the safe bar area. - const int degree_spacing = get_scaled_degree_spacing(config, params); - const double total_width = temp_ext.width + - (get_slot_is_temp(data, slot_value) - ? degree_spacing + degree_ext.width - : 0.0); - double temp_x = bar_right - total_width; - double final_temp_y = temp_y; - - // 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 only for temperature sensors - if (get_slot_is_temp(data, slot_value)) + if (verbose_logging) { - 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); + log_message( + LOG_INFO, + "Circle layout: slot=%s logical(height=%u gap=%u) scaled(height=%d gap=%.1f) bar_y=%d grouped_height=%.1f value_gap=%.1f value_box=%.1fx%.1f label_box_y=%.1f label_box_h=%.1f safe_width=%d", + get_slot_name_by_index(current_slot_index), + get_slot_bar_height(config, get_slot_name_by_index(current_slot_index)), + config->layout_bar_gap, bar_height, region_gap, bar_y, + grouped_height, value_bar_gap, value_box_y, value_box_height, label_box_y, + label_box_height, effective_bar_width); } + const Color *value_color = &config->font_color_temp; + // Draw temperature bar (centered reference point) const double bar_alpha = config->layout_bar_opacity; @@ -253,7 +439,7 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config, set_cairo_color_alpha(cr, &config->layout_bar_color_border, bar_alpha); draw_rounded_rectangle_path(cr, bar_x, bar_y, effective_bar_width, bar_height, params->corner_radius); - cairo_set_line_width(cr, config->layout_bar_border); + cairo_set_line_width(cr, get_scaled_bar_border_width(config, params)); cairo_stroke(cr); } @@ -274,37 +460,228 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config, cairo_restore(cr); } - // Draw label (CPU, GPU, or LIQ) - centered horizontally, close to bottom + cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + set_cairo_color(cr, value_color); + SlotValueLayout value_layout = {0}; + if (value_box_height > 0.0) + { + double safe_x = bar_x; + double safe_width = effective_bar_width; + calculate_text_lane_bounds(config, params, value_box_y, + value_box_height, 0, bar_x, + effective_bar_width, &safe_x, + &safe_width); + layout_and_render_slot_value(cr, data, config, params, slot_value, + temp_value, safe_x, value_box_y, + safe_width, value_box_height, 0, + 1, &value_layout); + + /* Duty % rendered left-aligned at half temp font size */ + if (value_layout.active && + (strcmp(slot_value, "cpu") == 0 || + strcmp(slot_value, "gpu") == 0)) + { + const sensor_entry_t *duty_s = find_channel_sensor_for_slot( + data, slot_value, SENSOR_CATEGORY_DUTY); + if (duty_s) + { + char duty_buf[16]; + snprintf(duty_buf, sizeof(duty_buf), "%.0f%%", + duty_s->value); + + double duty_font = value_layout.font_size * 0.5; + cairo_font_extents_t duty_fext = {0}; + cairo_text_extents_t duty_text_ext = {0}; + + cairo_select_font_face(cr, config->font_face, + CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(cr, duty_font); + cairo_font_extents(cr, &duty_fext); + cairo_text_extents(cr, duty_buf, &duty_text_ext); + + /* Position: left margin, vertically centered with temp */ + const double left_margin_factor = + (config->layout_label_margin_left > 0) + ? (config->layout_label_margin_left / 100.0) + : 0.01; + double duty_x = safe_x + + (safe_width * left_margin_factor); + double duty_y = value_layout.baseline_y; + + /* Don't overlap with temperature block */ + double duty_right = duty_x + + fmax(duty_text_ext.x_advance, duty_text_ext.width) + + scale_value_avg(params, 4.0); + if (duty_right <= value_layout.block_left) + { + set_cairo_color(cr, value_color); + cairo_move_to(cr, duty_x, duty_y); + cairo_show_text(cr, duty_buf); + } + } + } + } + + // Draw label (CPU, GPU, or LIQ) in a dedicated bottom lane anchored to the bar. if (label_text) { const Color *label_color = &config->font_color_label; + double label_safe_x = bar_x; + double label_safe_width = effective_bar_width; + const double left_margin_factor = + (config->layout_label_margin_left > 0) + ? (config->layout_label_margin_left / 100.0) + : 0.01; + + calculate_text_lane_bounds(config, params, label_box_y, + label_box_height, 0, bar_x, + effective_bar_width, &label_safe_x, + &label_safe_width); + + const double label_left_padding = label_safe_width * left_margin_factor; + const double label_inner_padding_y = + fmax(2.0, scale_value_avg(params, 4.0)); + const double available_label_width = + fmax(24.0, label_safe_width - label_left_padding); + const double available_label_height = + fmax(12.0, label_box_height - (2.0 * label_inner_padding_y)); + const double min_label_font_size = + (config->font_size_labels > 0.0f) + ? fmax(12.0, scale_value_avg(params, + (double)config->font_size_labels) * + 0.70) + : fmax(12.0, scale_value_avg(params, 12.0)); cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, config->font_size_labels); - set_cairo_color(cr, label_color); - cairo_text_extents_t label_text_ext; - cairo_text_extents(cr, label_text, &label_text_ext); + while (1) + { + cairo_set_font_size(cr, label_font_size); + cairo_font_extents(cr, &label_font_ext); + cairo_text_extents(cr, label_text, &label_text_ext); + + if ((fmax(label_text_ext.x_advance, label_text_ext.width) <= + available_label_width && + (label_font_ext.ascent + label_font_ext.descent) <= + available_label_height) || + label_font_size <= min_label_font_size) + break; + + label_font_size *= 0.94; + if (label_font_size < min_label_font_size) + label_font_size = min_label_font_size; + } - const double left_margin_factor = - (config->layout_label_margin_left > 0) - ? (config->layout_label_margin_left / 100.0) - : 0.01; - double label_x = config->display_width * left_margin_factor; + set_cairo_color(cr, label_color); - // Position label 2% from bottom - double final_label_y = config->display_height - (config->display_height * 0.02); + double label_x = label_safe_x + (label_safe_width * left_margin_factor); + double final_label_y = + label_box_y + label_inner_padding_y + label_font_ext.ascent; - // Apply user-defined offsets - if (config->display_label_offset_x != 0) - label_x += config->display_label_offset_x; - if (config->display_label_offset_y != 0) - final_label_y += config->display_label_offset_y; + // Apply user-defined offsets using the uniform layout scale. + label_x += get_scaled_label_offset_x(config, params); + final_label_y += get_scaled_label_offset_y(config, params); cairo_move_to(cr, label_x, final_label_y); cairo_show_text(cr, label_text); } + + // Draw extra info (freq/watts/RPM) below the label if enabled + if (config->circle_show_extra_info) + { + char extra_buf[64]; + if (get_extra_info_text(data, slot_value, extra_buf, sizeof(extra_buf))) + { + double extra_font_size = label_font_size * 2.2; + const double extra_padding_top = fmax(1.0, scale_value_avg(params, 3.0)); + + cairo_font_extents_t extra_font_ext = {0}; + cairo_text_extents_t extra_text_ext = {0}; + + cairo_select_font_face(cr, config->font_face, + CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(cr, extra_font_size); + cairo_font_extents(cr, &extra_font_ext); + cairo_text_extents(cr, extra_buf, &extra_text_ext); + + // Auto-shrink if text exceeds available width + const double extra_available_width = + fmax(24.0, (double)params->safe_bar_width * 0.96); + const double min_extra_font = label_font_size * 0.8; + while (fmax(extra_text_ext.x_advance, extra_text_ext.width) > + extra_available_width && + extra_font_size > min_extra_font) + { + extra_font_size *= 0.94; + cairo_set_font_size(cr, extra_font_size); + cairo_font_extents(cr, &extra_font_ext); + cairo_text_extents(cr, extra_buf, &extra_text_ext); + } + + double extra_y = label_box_y + label_font_ext.ascent + + label_font_ext.descent + + extra_padding_top + extra_font_ext.ascent; + + // Only render if it fits within the display height + if (extra_y + extra_font_ext.descent <= config->display_height) + { + const Color *value_col = &config->font_color_temp; + set_cairo_color(cr, value_col); + + const double left_margin_factor = + (config->layout_label_margin_left > 0) + ? (config->layout_label_margin_left / 100.0) + : 0.01; + double extra_x = (int)lround(params->safe_content_margin) + + ((double)params->safe_bar_width * left_margin_factor) + + get_scaled_label_offset_x(config, params); + + cairo_move_to(cr, extra_x, extra_y); + cairo_show_text(cr, extra_buf); + + // Second line: fan RPM for CPU/GPU + char line2_buf[64]; + if (get_extra_info_line2(data, slot_value, line2_buf, + sizeof(line2_buf))) + { + double line2_font_size = extra_font_size; + cairo_font_extents_t line2_font_ext = {0}; + cairo_text_extents_t line2_text_ext = {0}; + + cairo_set_font_size(cr, line2_font_size); + cairo_font_extents(cr, &line2_font_ext); + cairo_text_extents(cr, line2_buf, &line2_text_ext); + + // Auto-shrink for line 2 + while (fmax(line2_text_ext.x_advance, + line2_text_ext.width) > + extra_available_width && + line2_font_size > min_extra_font) + { + line2_font_size *= 0.94; + cairo_set_font_size(cr, line2_font_size); + cairo_font_extents(cr, &line2_font_ext); + cairo_text_extents(cr, line2_buf, &line2_text_ext); + } + + double line2_y = extra_y + extra_font_ext.descent + + extra_padding_top + line2_font_ext.ascent; + + if (line2_y + line2_font_ext.descent <= + config->display_height) + { + set_cairo_color(cr, value_col); + cairo_move_to(cr, extra_x, line2_y); + cairo_show_text(cr, line2_buf); + } + } + } + } + } } /** diff --git a/src/mods/display.c b/src/mods/display.c index c6c1b9d..ad3dbd3 100644 --- a/src/mods/display.c +++ b/src/mods/display.c @@ -37,6 +37,15 @@ #define M_SQRT1_2 0.7071067811865476 #endif +static double clamp_double(double value, double min_value, double max_value) +{ + if (value < min_value) + return min_value; + if (value > max_value) + return max_value; + return value; +} + /** * @brief Convert color component into a 0.0-1.0 range. */ @@ -92,7 +101,7 @@ double scale_value_avg(const ScalingParams *params, double value) { if (!params) return value; - return value * ((params->scale_x + params->scale_y) / 2.0); + return value * params->scale_uniform; } /** @@ -110,6 +119,381 @@ int get_scaled_degree_spacing(const struct Config *config, return (scaled_spacing > 0) ? scaled_spacing : 1; } +/** + * @brief Get the scaled bar border width. + */ +double get_scaled_bar_border_width(const struct Config *config, + const ScalingParams *params) +{ + if (!config || config->layout_bar_border <= 0.0f) + return 0.0; + + const double scaled_border = scale_value_avg(params, + (double)config->layout_bar_border); + return (scaled_border > 0.0) ? scaled_border : 0.0; +} + +/** + * @brief Get the preferred label font size. + */ +double get_preferred_label_font_size(const struct Config *config, + const ScalingParams *params) +{ + if (!config) + return 0.0; + + if (config->font_size_labels > 0.0f) + return scale_value_avg(params, (double)config->font_size_labels); + + return scale_value_avg(params, 28.0); +} + +/** + * @brief Get the scaled global label X offset using uniform scaling. + */ +int get_scaled_label_offset_x(const struct Config *config, + const ScalingParams *params) +{ + if (!config) + return 0; + + return (int)lround(scale_value_avg( + params, (double)config->display_label_offset_x)); +} + +/** + * @brief Get the scaled global label Y offset using uniform scaling. + */ +int get_scaled_label_offset_y(const struct Config *config, + const ScalingParams *params) +{ + if (!config) + return 0; + + return (int)lround(scale_value_avg( + params, (double)config->display_label_offset_y)); +} + +/** + * @brief Get the scaled pixel bar gap. + */ +int get_scaled_bar_gap(const struct Config *config, + const ScalingParams *params) +{ + if (!config) + return 0; + + const int scaled_gap = (int)lround(scale_value_avg( + params, (double)config->layout_bar_gap)); + return (scaled_gap > 0) ? scaled_gap : 0; +} + +/** + * @brief Get the effective vertical spacing between bars and labels. + */ +double get_effective_label_spacing(const struct Config *config, + const ScalingParams *params) +{ + if (!config) + return 0.0; + + const double spacing_factor = + config->layout_label_margin_bar / 100.0; + const double min_dimension = + (config->display_width < config->display_height) + ? (double)config->display_width + : (double)config->display_height; + const double scaled_spacing = min_dimension * spacing_factor; + const double minimum_spacing = scale_value_avg(params, 2.0); + + return fmax(minimum_spacing, scaled_spacing); +} + +/** + * @brief Format a sensor value with optional decimals. + */ +void format_slot_value_text(char *buffer, size_t buffer_size, + const monitor_sensor_data_t *data, + const char *slot_value, float temp_value) +{ + if (!buffer || buffer_size == 0) + return; + + if (get_slot_use_decimal(data, slot_value)) + snprintf(buffer, buffer_size, "%.1f", temp_value); + else + snprintf(buffer, buffer_size, "%d", (int)temp_value); +} + +/** + * @brief Measure and optionally render a right-aligned sensor value block. + */ +void layout_and_render_slot_value(cairo_t *cr, + const monitor_sensor_data_t *data, + const struct Config *config, + const ScalingParams *params, + const char *slot_value, + float temp_value, double box_x, + double box_y, double box_width, + double box_height, int align_bottom, + int draw_output, + SlotValueLayout *layout) +{ + if (!cr || !data || !config || !params || !slot_value || !layout || + box_width <= 0.0 || box_height <= 0.0) + return; + + memset(layout, 0, sizeof(*layout)); + + char value_text[16] = {0}; + format_slot_value_text(value_text, sizeof(value_text), data, slot_value, + temp_value); + + const int is_temp = get_slot_is_temp(data, slot_value); + const double offset_x = + scale_value_x(params, (double)get_slot_offset_x(config, slot_value)); + const double offset_y = + scale_value_y(params, (double)get_slot_offset_y(config, slot_value)); + const int degree_spacing = + is_temp ? ((get_scaled_degree_spacing(config, params) > 1) + ? (int)lround(get_scaled_degree_spacing(config, params) * 0.65) + : 1) + : get_scaled_degree_spacing(config, params); + const double temp_margin_factor = + (config->layout_label_margin_left > 0) + ? (config->layout_label_margin_left / 200.0) + : 0.005; + const double right_margin = fmax(0.0, box_width * temp_margin_factor); + const double inner_padding_x = clamp_double(scale_value_avg(params, 0.75), + 0.0, box_width * 0.015); + const double inner_padding_y = clamp_double(scale_value_avg(params, 0.25), + 0.0, box_height * 0.02); + const double growth_factor = + (config->font_growth_factor >= 1.0f) + ? (double)config->font_growth_factor + : 1.0; + const double available_width = + fmax(16.0, (box_width - inner_padding_x - right_margin) * growth_factor); + const double available_height = + fmax(12.0, (box_height - inner_padding_y) * growth_factor); + const double configured_font_size = get_slot_font_size(config, slot_value); + const double configured_scaled_font_size = + (configured_font_size > 0.0) + ? scale_value_avg(params, configured_font_size) + : 0.0; + const double auto_font_size = fmax(scale_value_avg(params, 220.0), + available_height * 7.40); + const double preferred_font_size = + (configured_scaled_font_size > 0.0) + ? configured_scaled_font_size + : auto_font_size; + const double min_font_size = + (configured_scaled_font_size > 0.0) + ? fmax(8.0, scale_value_avg(params, 8.0)) + : fmax(12.0, scale_value_avg(params, 14.0)); + + cairo_text_extents_t number_ext = {0}; + cairo_text_extents_t degree_ext = {0}; + cairo_font_extents_t font_ext = {0}; + double font_size = preferred_font_size; + double degree_font_size = font_size / 2.05; + double total_width = 0.0; + double number_width = 0.0; + double content_height = 0.0; + + while (1) + { + cairo_set_font_size(cr, font_size); + cairo_font_extents(cr, &font_ext); + cairo_text_extents(cr, value_text, &number_ext); + number_width = fmax(number_ext.x_advance, number_ext.width); + + degree_font_size = font_size / 2.05; + degree_ext = (cairo_text_extents_t){0}; + if (is_temp) + { + cairo_set_font_size(cr, degree_font_size); + cairo_text_extents(cr, "\xC2\xB0", °ree_ext); + cairo_set_font_size(cr, font_size); + } + + total_width = number_width + + (is_temp + ? (degree_spacing + + fmax(degree_ext.x_advance, degree_ext.width)) + : 0.0); + content_height = fmax(number_ext.height, + font_ext.ascent + font_ext.descent); + + if ((total_width <= available_width && + content_height <= available_height) || + font_size <= min_font_size) + break; + + font_size *= 0.93; + if (font_size < min_font_size) + font_size = min_font_size; + } + + if (configured_scaled_font_size <= 0.0) + { + double growth_step = fmax(0.5, scale_value_avg(params, 1.0)); + + while (growth_step >= 0.25) + { + while (1) + { + const double candidate_font_size = font_size + growth_step; + cairo_text_extents_t candidate_number_ext = {0}; + cairo_text_extents_t candidate_degree_ext = {0}; + cairo_font_extents_t candidate_font_ext = {0}; + double candidate_number_width = 0.0; + double candidate_total_width = 0.0; + double candidate_content_height = 0.0; + double candidate_degree_font_size = candidate_font_size / 2.05; + + cairo_set_font_size(cr, candidate_font_size); + cairo_font_extents(cr, &candidate_font_ext); + cairo_text_extents(cr, value_text, &candidate_number_ext); + candidate_number_width = + fmax(candidate_number_ext.x_advance, + candidate_number_ext.width); + + if (is_temp) + { + cairo_set_font_size(cr, candidate_degree_font_size); + cairo_text_extents(cr, "\xC2\xB0", &candidate_degree_ext); + cairo_set_font_size(cr, candidate_font_size); + } + + candidate_total_width = candidate_number_width + + (is_temp + ? (degree_spacing + + fmax(candidate_degree_ext.x_advance, + candidate_degree_ext.width)) + : 0.0); + candidate_content_height = + fmax(candidate_number_ext.height, + candidate_font_ext.ascent + + candidate_font_ext.descent); + + if (candidate_total_width > available_width || + candidate_content_height > available_height) + break; + + font_size = candidate_font_size; + degree_font_size = candidate_degree_font_size; + number_ext = candidate_number_ext; + degree_ext = candidate_degree_ext; + font_ext = candidate_font_ext; + number_width = candidate_number_width; + total_width = candidate_total_width; + content_height = candidate_content_height; + } + + growth_step *= 0.5; + } + } + + const double requested_right = + box_x + box_width - right_margin + offset_x; + const double block_left = + fmax(box_x + inner_padding_x + offset_x, requested_right - total_width); + const double number_x = block_left - number_ext.x_bearing; + + double baseline_y; + if (align_bottom) + { + const double ink_bottom = + box_y + box_height - inner_padding_y + offset_y; + baseline_y = ink_bottom - (number_ext.y_bearing + number_ext.height); + } + else + { + const double ink_top = box_y + inner_padding_y + offset_y; + baseline_y = ink_top - number_ext.y_bearing; + } + + if (draw_output) + { + cairo_set_font_size(cr, font_size); + cairo_move_to(cr, number_x, baseline_y); + cairo_show_text(cr, value_text); + + if (is_temp) + { + const double number_top = baseline_y + number_ext.y_bearing; + const double degree_top = number_top + (number_ext.height * 0.08); + const double degree_x = + block_left + number_width + degree_spacing - degree_ext.x_bearing; + const double degree_y = degree_top - degree_ext.y_bearing; + + cairo_set_font_size(cr, degree_font_size); + cairo_move_to(cr, degree_x, degree_y); + cairo_show_text(cr, "\xC2\xB0"); + cairo_set_font_size(cr, font_size); + } + } + + layout->active = 1; + layout->block_left = block_left; + layout->block_right = block_left + total_width; + layout->block_top = baseline_y + number_ext.y_bearing; + layout->block_bottom = layout->block_top + number_ext.height; + layout->baseline_y = baseline_y; + layout->font_size = font_size; +} + +/** + * @brief Calculate usable horizontal bounds for a text lane. + */ +void calculate_text_lane_bounds(const struct Config *config, + const ScalingParams *params, + double box_y, double box_height, + int align_bottom, + double fallback_x, double fallback_width, + double *safe_x, double *safe_width) +{ + if (!safe_x || !safe_width) + return; + + *safe_x = fallback_x; + *safe_width = fallback_width; + + if (!config || !params || !params->is_circular || box_height <= 0.0) + return; + + const double min_dimension = + (config->display_width < config->display_height) + ? (double)config->display_width + : (double)config->display_height; + const double content_scale = + (config->display_content_scale_factor > 0.0f && + config->display_content_scale_factor <= 1.0f) + ? config->display_content_scale_factor + : 0.98; + const double radius = (min_dimension * 0.5) * content_scale; + const double center_x = config->display_width / 2.0; + const double center_y = config->display_height / 2.0; + const double anchor_ratio = align_bottom ? 0.82 : 0.18; + const double sample_y = box_y + (box_height * anchor_ratio); + const double limiting_distance = fabs(sample_y - center_y); + + if (limiting_distance >= radius) + return; + + const double half_width = sqrt((radius * radius) - + (limiting_distance * limiting_distance)); + const double candidate_x = center_x - half_width; + const double candidate_width = half_width * 2.0; + + if (candidate_width > 0.0) + { + *safe_x = candidate_x; + *safe_width = candidate_width; + } +} + /** * @brief Paint a configured overlay over a loaded background image. */ @@ -297,85 +681,23 @@ void calculate_scaling_params(const struct Config *config, { const double base_width = 240.0; const double base_height = 240.0; + const double min_dimension = + (config->display_width < config->display_height) + ? (double)config->display_width + : (double)config->display_height; + const double base_min_dimension = + (base_width < base_height) ? base_width : base_height; params->scale_x = config->display_width / base_width; params->scale_y = config->display_height / base_height; - const double scale_avg = (params->scale_x + params->scale_y) / 2.0; + params->scale_uniform = + (base_min_dimension > 0.0) ? (min_dimension / base_min_dimension) : 1.0; - // Detect circular displays using device database with resolution info - int is_circular_by_device = is_circular_display_device( + params->is_circular = is_circular_display_device( device_name, config->display_width, config->display_height); + params->inscribe_factor = params->is_circular ? M_SQRT1_2 : 1.0; - // Check display_shape configuration - if (strcmp(config->display_shape, "rectangular") == 0) - { - // Force rectangular (inscribe_factor = 1.0) - params->is_circular = 0; - params->inscribe_factor = 1.0; - log_message(LOG_INFO, "Display shape forced to rectangular via config " - "(inscribe_factor: 1.0)"); - } - else if (strcmp(config->display_shape, "circular") == 0) - { - // Force circular (inscribe_factor = M_SQRT1_2 ~ 0.7071) - params->is_circular = 1; - double cfg_inscribe; - if (config->display_inscribe_factor == 0.0f) - cfg_inscribe = M_SQRT1_2; - else if (config->display_inscribe_factor > 0.0f && - config->display_inscribe_factor <= 1.0f) - cfg_inscribe = (double)config->display_inscribe_factor; - else - cfg_inscribe = M_SQRT1_2; - params->inscribe_factor = cfg_inscribe; - log_message( - LOG_INFO, - "Display shape forced to circular via config (inscribe_factor: %.4f)", - params->inscribe_factor); - } - else if (config->force_display_circular) - { - // Legacy developer override (CLI --develop) - params->is_circular = 1; - { - double cfg_inscribe; - if (config->display_inscribe_factor == 0.0f) - cfg_inscribe = M_SQRT1_2; - else if (config->display_inscribe_factor > 0.0f && - config->display_inscribe_factor <= 1.0f) - cfg_inscribe = (double)config->display_inscribe_factor; - else - cfg_inscribe = M_SQRT1_2; - params->inscribe_factor = cfg_inscribe; - } - log_message(LOG_INFO, - "Developer override active: forcing circular display detection " - "(device: %s)", - device_name ? device_name : "unknown"); - } - else - { - // Auto-detection based on device database - params->is_circular = is_circular_by_device; - if (params->is_circular) - { - double cfg_inscribe; - if (config->display_inscribe_factor == 0.0f) - cfg_inscribe = M_SQRT1_2; - else if (config->display_inscribe_factor > 0.0f && - config->display_inscribe_factor <= 1.0f) - cfg_inscribe = (double)config->display_inscribe_factor; - else - cfg_inscribe = M_SQRT1_2; - params->inscribe_factor = cfg_inscribe; - } - else - { - params->inscribe_factor = 1.0; - } - } - - // Calculate safe area width + // Calculate safe area width from detected device geometry only. const double safe_area_width = config->display_width * params->inscribe_factor; const float content_scale = (config->display_content_scale_factor > 0.0f && @@ -391,13 +713,17 @@ void calculate_scaling_params(const struct Config *config, params->safe_content_margin = (config->display_width - params->safe_bar_width) / 2.0; - params->corner_radius = 8.0 * scale_avg; + params->corner_radius = 8.0 * params->scale_uniform; // Log detailed scaling calculations log_message( LOG_INFO, - "Scaling: safe_area=%.0fpx, bar_width=%dpx (%.0f%%), margin=%.1fpx", - safe_area_width, params->safe_bar_width, bar_width_factor * 100.0, + "Scaling: display=%ux%u scale=(%.3f, %.3f) uniform=%.3f shape=%s inscribe=%.4f content=%.3f safe_area=%.0fpx bar_width=%dpx (%.0f%%) margin=%.1fpx", + config->display_width, config->display_height, params->scale_x, + params->scale_y, params->scale_uniform, + params->is_circular ? "circular" : "rectangular", + params->inscribe_factor, content_scale, safe_area_width, + params->safe_bar_width, bar_width_factor * 100.0, params->safe_content_margin); } @@ -446,7 +772,34 @@ const char *get_slot_label(const struct Config *config, return sc->label; } - /* Legacy labels */ + /* Use device name from API if available (first two words only) */ + if (data) + { + const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value); + if (entry && entry->device_name[0] != '\0') + { + static char short_name[SENSOR_DEVICE_NAME_LEN]; + cc_safe_strcpy(short_name, sizeof(short_name), + entry->device_name); + /* Find end of second word */ + int words = 0; + for (size_t i = 0; short_name[i] != '\0'; i++) + { + if (short_name[i] == ' ') + { + words++; + if (words >= 2) + { + short_name[i] = '\0'; + break; + } + } + } + return short_name; + } + } + + /* Fallback: legacy labels */ if (strcmp(slot_value, "cpu") == 0) return "CPU"; if (strcmp(slot_value, "gpu") == 0) @@ -514,16 +867,42 @@ uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name) if (!config || !slot_name) return 24; - if (strcmp(slot_name, "up") == 0) - return config->layout_bar_height_up; - else if (strcmp(slot_name, "mid") == 0) - return config->layout_bar_height_mid; - else if (strcmp(slot_name, "down") == 0) - return config->layout_bar_height_down; + if (strcmp(slot_name, "1") == 0) + { + return (config->layout_bar_height_1 > 0) + ? config->layout_bar_height_1 + : config->layout_bar_height; + } + else if (strcmp(slot_name, "2") == 0) + { + return (config->layout_bar_height_2 > 0) + ? config->layout_bar_height_2 + : config->layout_bar_height; + } + else if (strcmp(slot_name, "3") == 0) + { + return (config->layout_bar_height_3 > 0) + ? config->layout_bar_height_3 + : config->layout_bar_height; + } return config->layout_bar_height; } +/** + * @brief Get the scaled pixel bar height for a slot. + */ +int get_scaled_slot_bar_height(const struct Config *config, + const ScalingParams *params, + const char *slot_name) +{ + const uint16_t logical_height = get_slot_bar_height(config, slot_name); + const int scaled_height = + (int)lround(scale_value_avg(params, (double)logical_height)); + + return (scaled_height > 0) ? scaled_height : 1; +} + /** * @brief Get display unit string for a sensor slot. */ diff --git a/src/mods/display.h b/src/mods/display.h index 43cfede..737d068 100644 --- a/src/mods/display.h +++ b/src/mods/display.h @@ -55,6 +55,7 @@ typedef struct { double scale_x; double scale_y; + double scale_uniform; double corner_radius; double inscribe_factor; /**< 1.0 for rectangular, M_SQRT1_2 for circular */ int safe_bar_width; /**< Safe bar width for circular displays */ @@ -62,6 +63,20 @@ typedef struct int is_circular; /**< 1 if circular display, 0 if rectangular */ } ScalingParams; +/** + * @brief Measured layout for a rendered sensor value block. + */ +typedef struct +{ + int active; + double block_left; + double block_right; + double block_top; + double block_bottom; + double baseline_y; + double font_size; +} SlotValueLayout; + /** * @brief Main display dispatcher - routes to appropriate rendering mode. * @details High-level entry point that examines configuration and dispatches to @@ -131,6 +146,95 @@ double scale_value_avg(const ScalingParams *params, double value); int get_scaled_degree_spacing(const struct Config *config, const ScalingParams *params); +/** + * @brief Get the effective logical bar height for a slot. + * @details Slot-specific values inherit the global bar height when set to 0. + */ +uint16_t get_slot_bar_height(const struct Config *config, + const char *slot_name); + +/** + * @brief Get the scaled pixel bar height for a slot. + */ +int get_scaled_slot_bar_height(const struct Config *config, + const ScalingParams *params, + const char *slot_name); + +/** + * @brief Get the scaled pixel bar gap. + */ +int get_scaled_bar_gap(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Get the effective vertical spacing between bars and labels. + */ +double get_effective_label_spacing(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Get the scaled bar border width. + */ +double get_scaled_bar_border_width(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Get the preferred label font size. + * @details A configured label size is scaled uniformly. A value of 0 enables + * renderer-driven automatic sizing. + */ +double get_preferred_label_font_size(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Get the scaled global label X offset using uniform scaling. + */ +int get_scaled_label_offset_x(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Get the scaled global label Y offset using uniform scaling. + */ +int get_scaled_label_offset_y(const struct Config *config, + const ScalingParams *params); + +/** + * @brief Format a sensor value using the configured decimal mode. + */ +void format_slot_value_text(char *buffer, size_t buffer_size, + const monitor_sensor_data_t *data, + const char *slot_value, float temp_value); + +/** + * @brief Measure and optionally render a right-aligned sensor value block. + * @details Fits the font size to the available box width and height and places + * the value against the right edge of the provided box. Offsets remain as a + * user-facing fine-tuning layer. + */ +void layout_and_render_slot_value(cairo_t *cr, + const monitor_sensor_data_t *data, + const struct Config *config, + const ScalingParams *params, + const char *slot_value, + float temp_value, double box_x, + double box_y, double box_width, + double box_height, int align_bottom, + int draw_output, + SlotValueLayout *layout); + +/** + * @brief Get horizontal safe bounds for a text lane. + * @details On circular displays this computes the narrowest usable chord for + * the specified vertical region so text can use more space without clipping. + * Rectangular displays fall back to the given box bounds. + */ +void calculate_text_lane_bounds(const struct Config *config, + const ScalingParams *params, + double box_y, double box_height, + int align_bottom, + double fallback_x, double fallback_width, + double *safe_x, double *safe_width); + /** * @brief Paint display background from optional PNG image or fallback color. * @details If a PNG background path is configured and readable, the image is @@ -209,10 +313,10 @@ Color get_slot_bar_color(const struct Config *config, const char *slot_value, float get_slot_max_scale(const struct Config *config, const char *slot_value); /** - * @brief Get bar height for a specific slot. + * @brief Get the effective logical bar height for a slot. * @param config Configuration with bar height values - * @param slot_name Slot name: "up", "mid", or "down" - * @return Bar height in pixels for the specified slot + * @param slot_name Slot name: "1", "2", or "3" + * @return Bar height in logical layout units for the specified slot */ uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name); diff --git a/src/mods/dual.c b/src/mods/dual.c index 3490435..aff838d 100644 --- a/src/mods/dual.c +++ b/src/mods/dual.c @@ -53,195 +53,143 @@ static void render_display_content(cairo_t *cr, const struct Config *config, const monitor_sensor_data_t *data, const ScalingParams *params); -/** - * @brief Draw temperature displays for up and down slots. - */ -static void draw_temperature_displays(cairo_t *cr, - const monitor_sensor_data_t *data, - const struct Config *config, - const ScalingParams *params) +typedef struct { - if (!cr || !data || !config || !params) - return; - - const int effective_bar_width = params->safe_bar_width; - const int bar_x = (config->display_width - effective_bar_width) / 2; - const int bar_right = bar_x + effective_bar_width; - const int degree_spacing = get_scaled_degree_spacing(config, params); - const double top_temp_padding = scale_value_y(params, 8.0); - const double bottom_temp_padding = scale_value_y(params, 4.0); - - // Get slot configurations - const char *slot_up = config->sensor_slot_up; - const char *slot_down = config->sensor_slot_down; - const int up_active = slot_is_active(slot_up); - const int down_active = slot_is_active(slot_down); + int up_active; + int down_active; + int bar_x; + int effective_bar_width; + int bar_gap; + int up_bar_y; + int down_bar_y; + uint16_t bar_height_up; + uint16_t bar_height_down; + double label_spacing; + double top_value_box_y; + double top_value_box_height; + double bottom_value_box_y; + double bottom_value_box_height; +} DualLayout; + +static int calculate_dual_layout(const struct Config *config, + const ScalingParams *params, + DualLayout *layout) +{ + if (!config || !params || !layout) + return 0; - // Get temperatures from configured slots - float temp_up = get_slot_temperature(data, slot_up); - float temp_down = get_slot_temperature(data, slot_down); + memset(layout, 0, sizeof(*layout)); - // Get bar heights - const uint16_t bar_height_up = get_slot_bar_height(config, "up"); - const uint16_t bar_height_down = get_slot_bar_height(config, "down"); + layout->effective_bar_width = params->safe_bar_width; + layout->bar_x = (int)lround(params->safe_content_margin); + layout->up_active = slot_is_active(config->sensor_slot_1); + layout->down_active = slot_is_active(config->sensor_slot_3); + layout->bar_height_up = (uint16_t)get_scaled_slot_bar_height(config, params, "1"); + layout->bar_height_down = (uint16_t)get_scaled_slot_bar_height(config, params, "3"); + layout->bar_gap = get_scaled_bar_gap(config, params); - // Calculate bar positions based on active slots int total_height = 0; - if (up_active && down_active) - total_height = bar_height_up + config->layout_bar_gap + bar_height_down; - else if (up_active) - total_height = bar_height_up; - else if (down_active) - total_height = bar_height_down; + if (layout->up_active && layout->down_active) + total_height = layout->bar_height_up + layout->bar_gap + + layout->bar_height_down; + else if (layout->up_active) + total_height = layout->bar_height_up; + else if (layout->down_active) + total_height = layout->bar_height_down; else - return; // No active slots - - const int start_y = (config->display_height - total_height) / 2; - int up_bar_y = start_y; - int down_bar_y = start_y + bar_height_up + config->layout_bar_gap; - - // If only down slot is active, center it - if (!up_active && down_active) - down_bar_y = start_y; - - // 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); + return 0; - cairo_font_extents_t up_font_ext; - cairo_font_extents(cr, &up_font_ext); + const int start_y = + (int)lround(((double)config->display_height - total_height) / 2.0); + layout->up_bar_y = start_y; + layout->down_bar_y = start_y + layout->bar_height_up + + layout->bar_gap; - // 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); + if (!layout->up_active && layout->down_active) + layout->down_bar_y = start_y; - char up_num_str[16]; - 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); + layout->label_spacing = get_effective_label_spacing(config, params); + const double value_bar_gap = layout->label_spacing * 0.05; - cairo_text_extents_t up_num_ext; - cairo_text_extents(cr, up_num_str, &up_num_ext); + layout->top_value_box_y = 0.0; + layout->top_value_box_height = + fmax(0.0, layout->up_bar_y - value_bar_gap); + layout->bottom_value_box_y = + layout->down_bar_y + layout->bar_height_down + value_bar_gap; + layout->bottom_value_box_height = + fmax(0.0, config->display_height - layout->bottom_value_box_y); - double up_num_width = - (temp_up >= 100.0f) ? up_num_ext.width : up_ref_ext.width; - double up_degree_width = 0.0; + if (verbose_logging) + { + log_message( + LOG_INFO, + "Dual layout: logical(up=%u, down=%u, gap=%u) scaled(up=%u, down=%u, gap=%d) start_y=%d up_y=%d down_y=%d label_spacing=%.1f value_gap=%.1f safe_width=%d margin=%.1f", + get_slot_bar_height(config, "1"), + get_slot_bar_height(config, "3"), config->layout_bar_gap, + layout->bar_height_up, layout->bar_height_down, layout->bar_gap, + start_y, layout->up_bar_y, layout->down_bar_y, + layout->label_spacing, value_bar_gap, layout->effective_bar_width, + params->safe_content_margin); + } - if (get_slot_is_temp(data, slot_up)) - { - cairo_set_font_size(cr, up_font_size / 1.66); - cairo_text_extents_t up_degree_ext; - cairo_text_extents(cr, "\xC2\xB0", &up_degree_ext); - up_degree_width = up_degree_ext.width; - cairo_set_font_size(cr, up_font_size); - } + return 1; +} - const double up_total_width = up_num_width + - (get_slot_is_temp(data, slot_up) - ? degree_spacing + up_degree_width - : 0.0); - const double up_block_x = bar_right - up_total_width; - double up_temp_x = up_block_x + (up_num_width - up_num_ext.width) / 2.0; +/** + * @brief Draw temperature displays for up and down slots. + */ +static void draw_temperature_displays(cairo_t *cr, + const monitor_sensor_data_t *data, + const struct Config *config, + const ScalingParams *params) +{ + if (!cr || !data || !config || !params) + return; - int offset_x_up = get_slot_offset_x(config, slot_up); - if (offset_x_up != 0) - up_temp_x += offset_x_up; + DualLayout layout = {0}; + if (!calculate_dual_layout(config, params, &layout)) + return; - double up_temp_y = up_bar_y + top_temp_padding - 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; + const char *slot_up = config->sensor_slot_1; + const char *slot_down = config->sensor_slot_3; - cairo_move_to(cr, up_temp_x, up_temp_y); - cairo_show_text(cr, up_num_str); + float temp_up = get_slot_temperature(data, slot_up); + float temp_down = get_slot_temperature(data, slot_down); - // Draw degree symbol or unit - if (get_slot_is_temp(data, slot_up)) - { - double degree_up_x = up_block_x + up_num_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); - } + if (layout.up_active && layout.top_value_box_height > 0.0) + { + double safe_x = layout.bar_x; + double safe_width = layout.effective_bar_width; + calculate_text_lane_bounds(config, params, layout.top_value_box_y, + layout.top_value_box_height, 0, + layout.bar_x, + layout.effective_bar_width, &safe_x, + &safe_width); + SlotValueLayout up_layout; + layout_and_render_slot_value(cr, data, config, params, slot_up, + temp_up, safe_x, + layout.top_value_box_y, + safe_width, + layout.top_value_box_height, 0, 1, + &up_layout); } - // Draw lower slot temperature - if (down_active) + if (layout.down_active && layout.bottom_value_box_height > 0.0) { - // 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]; - 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_num_width = - (temp_down >= 100.0f) ? down_num_ext.width : down_ref_ext.width; - double down_degree_width = 0.0; - - if (get_slot_is_temp(data, slot_down)) - { - cairo_set_font_size(cr, down_font_size / 1.66); - cairo_text_extents_t down_degree_ext; - cairo_text_extents(cr, "\xC2\xB0", &down_degree_ext); - down_degree_width = down_degree_ext.width; - cairo_set_font_size(cr, down_font_size); - } - - const double down_total_width = - down_num_width + - (get_slot_is_temp(data, slot_down) - ? degree_spacing + down_degree_width - : 0.0); - const double down_block_x = bar_right - down_total_width; - double down_temp_x = - down_block_x + (down_num_width - down_num_ext.width) / 2.0; - - 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 - - bottom_temp_padding + 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); - - // Draw degree symbol or unit - if (get_slot_is_temp(data, slot_down)) - { - double degree_down_x = - down_block_x + down_num_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); - } + double safe_x = layout.bar_x; + double safe_width = layout.effective_bar_width; + calculate_text_lane_bounds(config, params, layout.bottom_value_box_y, + layout.bottom_value_box_height, + 1, + layout.bar_x, layout.effective_bar_width, + &safe_x, &safe_width); + SlotValueLayout down_layout; + layout_and_render_slot_value(cr, data, config, params, slot_down, + temp_down, safe_x, + layout.bottom_value_box_y, + safe_width, + layout.bottom_value_box_height, 1, 1, + &down_layout); } } @@ -289,7 +237,7 @@ static void draw_single_temperature_bar_slot(cairo_t *cr, // Border (only if enabled and thickness > 0) if (config->layout_bar_border_enabled && config->layout_bar_border > 0.0f) { - cairo_set_line_width(cr, config->layout_bar_border); + cairo_set_line_width(cr, get_scaled_bar_border_width(config, params)); set_cairo_color_alpha(cr, &config->layout_bar_color_border, bar_alpha); draw_rounded_rectangle_path(cr, bar_x, bar_y, bar_width, bar_height, params->corner_radius); @@ -308,54 +256,29 @@ static void draw_temperature_bars(cairo_t *cr, if (!cr || !data || !config || !params) return; - const double bar_side_margin = config->display_width * 0.0025; - const int effective_bar_width = - params->safe_bar_width - (int)(2 * bar_side_margin); - const int bar_x = (config->display_width - effective_bar_width) / 2; - - // Get slot configurations - const char *slot_up = config->sensor_slot_up; - const char *slot_down = config->sensor_slot_down; - const int up_active = slot_is_active(slot_up); - const int down_active = slot_is_active(slot_down); - - // Get bar heights - const uint16_t bar_height_up = get_slot_bar_height(config, "up"); - const uint16_t bar_height_down = get_slot_bar_height(config, "down"); - - // Calculate positions based on active slots - int total_height = 0; - if (up_active && down_active) - total_height = bar_height_up + config->layout_bar_gap + bar_height_down; - else if (up_active) - total_height = bar_height_up; - else if (down_active) - total_height = bar_height_down; - else + DualLayout layout = {0}; + if (!calculate_dual_layout(config, params, &layout)) return; - const int start_y = (config->display_height - total_height) / 2; - int up_bar_y = start_y; - int down_bar_y = start_y + bar_height_up + config->layout_bar_gap; - - // If only down slot is active, center it - if (!up_active && down_active) - down_bar_y = start_y; + const char *slot_up = config->sensor_slot_1; + const char *slot_down = config->sensor_slot_3; - // Draw upper slot bar - if (up_active) + if (layout.up_active) { float temp_up = get_slot_temperature(data, slot_up); draw_single_temperature_bar_slot(cr, config, params, slot_up, temp_up, - bar_x, up_bar_y, effective_bar_width, bar_height_up); + layout.bar_x, layout.up_bar_y, + layout.effective_bar_width, + layout.bar_height_up); } - // Draw lower slot bar - if (down_active) + if (layout.down_active) { float temp_down = get_slot_temperature(data, slot_down); draw_single_temperature_bar_slot(cr, config, params, slot_down, temp_down, - bar_x, down_bar_y, effective_bar_width, bar_height_down); + layout.bar_x, layout.down_bar_y, + layout.effective_bar_width, + layout.bar_height_down); } } @@ -366,87 +289,162 @@ static void draw_labels(cairo_t *cr, const struct Config *config, const monitor_sensor_data_t *data, const ScalingParams *params) { - (void)data; - if (!cr || !config || !params) return; - // Get slot configurations - const char *slot_up = config->sensor_slot_up; - const char *slot_down = config->sensor_slot_down; - const int up_active = slot_is_active(slot_up); - const int down_active = slot_is_active(slot_down); + DualLayout layout = {0}; + if (!calculate_dual_layout(config, params, &layout)) + return; + + const char *slot_up = config->sensor_slot_1; + const char *slot_down = config->sensor_slot_3; - // Get labels from slots (NULL if "none") 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"); - const uint16_t bar_height_down = get_slot_bar_height(config, "down"); - - // Calculate total height based on active slots - int total_height = 0; - if (up_active && down_active) - total_height = bar_height_up + config->layout_bar_gap + bar_height_down; - else if (up_active) - total_height = bar_height_up; - else if (down_active) - total_height = bar_height_down; - else - return; - - const int start_y = (config->display_height - total_height) / 2; - int up_bar_y = start_y; - int down_bar_y = start_y + bar_height_up + config->layout_bar_gap; - - // If only down slot is active, center it - if (!up_active && down_active) - down_bar_y = start_y; + SlotValueLayout up_layout = {0}; + SlotValueLayout down_layout = {0}; + if (layout.up_active && layout.top_value_box_height > 0.0) + { + double safe_x = layout.bar_x; + double safe_width = layout.effective_bar_width; + calculate_text_lane_bounds(config, params, layout.top_value_box_y, + layout.top_value_box_height, 0, + layout.bar_x, + layout.effective_bar_width, &safe_x, + &safe_width); + layout_and_render_slot_value(cr, data, config, params, slot_up, + get_slot_temperature(data, slot_up), + safe_x, layout.top_value_box_y, + safe_width, + layout.top_value_box_height, 0, 0, + &up_layout); + } + if (layout.down_active && layout.bottom_value_box_height > 0.0) + { + double safe_x = layout.bar_x; + double safe_width = layout.effective_bar_width; + calculate_text_lane_bounds(config, params, layout.bottom_value_box_y, + layout.bottom_value_box_height, + 1, + layout.bar_x, layout.effective_bar_width, + &safe_x, &safe_width); + layout_and_render_slot_value(cr, data, config, params, slot_down, + get_slot_temperature(data, slot_down), + safe_x, layout.bottom_value_box_y, + safe_width, + layout.bottom_value_box_height, 1, 0, + &down_layout); + } // Labels: Configurable distance from left screen edge (default: 1%) const double left_margin_factor = (config->layout_label_margin_left > 0) ? (config->layout_label_margin_left / 100.0) : 0.01; - double label_x = config->display_width * left_margin_factor; + double label_x = params->safe_content_margin + + (layout.effective_bar_width * left_margin_factor); // Apply user-defined X offset if set - if (config->display_label_offset_x != -9999) - { - label_x += config->display_label_offset_x; - } + label_x += get_scaled_label_offset_x(config, params); cairo_font_extents_t font_ext; cairo_font_extents(cr, &font_ext); - // Vertical spacing: Configurable distance from bars (default: 1%) - const double bar_margin_factor = - (config->layout_label_margin_bar > 0) - ? (config->layout_label_margin_bar / 100.0) - : 0.01; - const double label_spacing = config->display_height * bar_margin_factor; - // Draw upper slot label (if active and has label) - if (up_active && label_up) + if (layout.up_active && label_up) { - double up_label_y = up_bar_y - label_spacing - font_ext.descent; - if (config->display_label_offset_y != -9999) - up_label_y += config->display_label_offset_y; + double label_font_size = get_preferred_label_font_size(config, params); + const double min_label_font_size = + (config->font_size_labels > 0.0f) + ? fmax(10.0, scale_value_avg(params, + (double)config->font_size_labels) * + 0.60) + : fmax(10.0, scale_value_avg(params, 10.0)); + cairo_text_extents_t up_label_ext; + while (1) + { + cairo_set_font_size(cr, label_font_size); + cairo_font_extents(cr, &font_ext); + cairo_text_extents(cr, label_up, &up_label_ext); + + const double up_safe_right = up_layout.active + ? (up_layout.block_left - scale_value_avg(params, 8.0)) + : (layout.bar_x + layout.effective_bar_width); + const double up_available_width = fmax(8.0, up_safe_right - label_x); + + if (fmax(up_label_ext.x_advance, up_label_ext.width) <= up_available_width || + label_font_size <= min_label_font_size) + break; + + label_font_size *= 0.92; + if (label_font_size < min_label_font_size) + label_font_size = min_label_font_size; + } + + double up_label_y = layout.up_bar_y - layout.label_spacing - font_ext.descent; + up_label_y += get_scaled_label_offset_y(config, params); - cairo_move_to(cr, label_x, up_label_y); - cairo_show_text(cr, label_up); + const double up_label_right = label_x + fmax(up_label_ext.x_advance, up_label_ext.width); + const double up_safe_right = up_layout.active + ? (up_layout.block_left - scale_value_avg(params, 8.0)) + : (layout.bar_x + layout.effective_bar_width); + if (up_label_right <= up_safe_right) + { + cairo_set_font_size(cr, label_font_size); + cairo_move_to(cr, label_x, up_label_y); + cairo_show_text(cr, label_up); + } } // Draw lower slot label (if active and has label) - if (down_active && label_down) + if (layout.down_active && label_down) { - double down_label_y = down_bar_y + bar_height_down + label_spacing + font_ext.ascent; - if (config->display_label_offset_y != -9999) - down_label_y += config->display_label_offset_y; + double label_font_size = get_preferred_label_font_size(config, params); + const double min_label_font_size = + (config->font_size_labels > 0.0f) + ? fmax(10.0, scale_value_avg(params, + (double)config->font_size_labels) * + 0.60) + : fmax(10.0, scale_value_avg(params, 10.0)); + cairo_text_extents_t down_label_ext; + while (1) + { + cairo_set_font_size(cr, label_font_size); + cairo_font_extents(cr, &font_ext); + cairo_text_extents(cr, label_down, &down_label_ext); + + const double down_safe_right = down_layout.active + ? (down_layout.block_left - scale_value_avg(params, 8.0)) + : (layout.bar_x + layout.effective_bar_width); + const double down_available_width = + fmax(8.0, down_safe_right - label_x); + + if (fmax(down_label_ext.x_advance, down_label_ext.width) <= + down_available_width || + label_font_size <= min_label_font_size) + break; + + label_font_size *= 0.92; + if (label_font_size < min_label_font_size) + label_font_size = min_label_font_size; + } - cairo_move_to(cr, label_x, down_label_y); - cairo_show_text(cr, label_down); + double down_label_y = layout.down_bar_y + layout.bar_height_down + + layout.label_spacing + font_ext.ascent; + down_label_y += get_scaled_label_offset_y(config, params); + + const double down_label_right = + label_x + fmax(down_label_ext.x_advance, down_label_ext.width); + const double down_safe_right = down_layout.active + ? (down_layout.block_left - scale_value_avg(params, 8.0)) + : (layout.bar_x + layout.effective_bar_width); + if (down_label_right <= down_safe_right) + { + cairo_set_font_size(cr, label_font_size); + cairo_move_to(cr, label_x, down_label_y); + cairo_show_text(cr, label_down); + } } } @@ -459,34 +457,18 @@ static void render_display_content(cairo_t *cr, const struct Config *config, { paint_display_background(cr, config); + draw_temperature_bars(cr, data, config, params); + 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); set_cairo_color(cr, &config->font_color_temp); draw_temperature_displays(cr, data, config, params); - draw_temperature_bars(cr, data, config, params); - // Get temperatures from active slots for label visibility check - 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 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); - draw_labels(cr, config, data, params); - } + cairo_set_font_size(cr, config->font_size_labels); + set_cairo_color(cr, &config->font_color_label); + draw_labels(cr, config, data, params); } /** diff --git a/src/srv/cc_conf.c b/src/srv/cc_conf.c index 248cad4..3e99ff0 100644 --- a/src/srv/cc_conf.c +++ b/src/srv/cc_conf.c @@ -17,6 +17,7 @@ // Include necessary headers // cppcheck-suppress-begin missingIncludeSystem +#include #include #include #include @@ -316,15 +317,205 @@ static int has_lcd_display(const json_t *dev) } /** - * @brief Search for the first CoolerControl LCD device in devices array. - * @details Selects the first device that exposes a valid UID and LCD - * dimensions through the CoolerControl API. + * @brief Convert ASCII character to lower-case without locale dependency. */ -static int search_lcd_device(const json_t *devices, char *lcd_uid, +static char ascii_tolower(char ch) +{ + return (char)tolower((unsigned char)ch); +} + +/** + * @brief Check if a string contains a token case-insensitively. + */ +static int contains_token_ci(const char *haystack, const char *needle) +{ + if (!haystack || !needle || needle[0] == '\0') + return 0; + + const size_t needle_len = strlen(needle); + for (size_t i = 0; haystack[i] != '\0'; i++) + { + size_t j = 0; + while (j < needle_len && haystack[i + j] != '\0' && + ascii_tolower(haystack[i + j]) == ascii_tolower(needle[j])) + { + j++; + } + if (j == needle_len) + return 1; + } + + return 0; +} + +/** + * @brief Match a pattern list against name, type, and UID. + * @details Patterns are separated by commas, semicolons, or newlines. + */ +static int pattern_list_matches(const char *pattern_list, const char *device_name, + const char *type_str, const char *device_uid) +{ + if (!pattern_list || pattern_list[0] == '\0') + return 0; + + char token[64]; + size_t token_len = 0; + + for (size_t i = 0;; i++) + { + const char ch = pattern_list[i]; + const int is_separator = + (ch == '\0' || ch == '\n' || ch == '\r' || ch == ',' || + ch == ';'); + + if (!is_separator) + { + if (!(token_len == 0 && isspace((unsigned char)ch)) && + token_len + 1 < sizeof(token)) + { + token[token_len++] = ch; + } + } + + if (is_separator) + { + while (token_len > 0 && + isspace((unsigned char)token[token_len - 1])) + { + token_len--; + } + + token[token_len] = '\0'; + if (token_len > 0 && + (contains_token_ci(device_name, token) || + contains_token_ci(type_str, token) || + contains_token_ci(device_uid, token))) + { + return 1; + } + + token_len = 0; + + if (ch == '\0') + break; + } + } + + return 0; +} + +/** + * @brief Detect common motherboard/mainboard false positives. + */ +static int looks_like_mainboard_device(const char *device_name) +{ + static const char *tokens[] = { + "mainboard", "motherboard", "chipset", "vrm", + "z690", "z790", "b650", "b660", + "x670", "x870", "x570", "b550"}; + + if (!device_name) + return 0; + + for (size_t i = 0; i < sizeof(tokens) / sizeof(tokens[0]); i++) + { + if (contains_token_ci(device_name, tokens[i])) + return 1; + } + + return 0; +} + +/** + * @brief Get effective LCD detection mode. + */ +static const char *get_detection_mode(const Config *config) +{ + if (!config || config->device_detection_mode[0] == '\0') + return "strict"; + return config->device_detection_mode; +} + +/** + * @brief Score an LCD candidate based on type, name, and dimensions. + */ +static int score_lcd_candidate(const Config *config, const char *device_uid, + const char *device_name, const char *type_str, + int screen_width, int screen_height) +{ + int score = 0; + const char *mode = get_detection_mode(config); + const int allowlisted = pattern_list_matches( + config ? config->device_detection_allowlist : NULL, device_name, + type_str, device_uid); + const int blocklisted = pattern_list_matches( + config ? config->device_detection_blocklist : NULL, device_name, + type_str, device_uid); + + if (allowlisted) + return 1000 + (screen_width * screen_height); + + if (blocklisted) + return -1000; + + if (type_str && strcmp(type_str, "Liquidctl") == 0) + score += 30; + else if (type_str && + (strcmp(type_str, "Hwmon") == 0 || + strcmp(type_str, "CustomSensors") == 0 || + strcmp(type_str, "CPU") == 0 || strcmp(type_str, "GPU") == 0)) + score -= 40; + else if (type_str && type_str[0] != '\0') + score += 8; + + if (contains_token_ci(device_name, "kraken") || + contains_token_ci(device_name, "elite") || + contains_token_ci(device_name, "hydro") || + contains_token_ci(device_name, "capellix") || + contains_token_ci(device_name, "lcd") || + contains_token_ci(device_name, "display") || + contains_token_ci(device_name, "screen") || + contains_token_ci(device_name, "aio")) + { + score += 24; + } + + if (looks_like_mainboard_device(device_name)) + score -= 80; + + if (screen_width == screen_height) + score += 6; + else + score += 3; + + if (screen_width >= 240 && screen_height >= 240) + score += 4; + + if (strcmp(mode, "relaxed") == 0) + return (score < 0) ? 0 : score; + + if (strcmp(mode, "balanced") == 0) + return (score >= 0) ? score : -1; + + if (looks_like_mainboard_device(device_name)) + return -1; + + return (score >= 20) ? score : -1; +} + +/** + * @brief Search for the best CoolerControl LCD device candidate. + * @details Uses UID, LCD metadata, and configurable allow/block heuristics. + */ +static int search_lcd_device(const Config *config, const json_t *devices, char *lcd_uid, size_t uid_size, int *found_lcd_device, int *screen_width, int *screen_height, char *device_name, size_t name_size) { + const json_t *best_dev = NULL; + int best_score = -1001; + int candidate_count = 0; + const size_t device_count = json_array_size(devices); for (size_t i = 0; i < device_count; i++) { @@ -335,6 +526,9 @@ static int search_lcd_device(const json_t *devices, char *lcd_uid, const char *type_str = extract_device_type_from_json(dev); const json_t *name_val = json_object_get(dev, "name"); const char *name = name_val ? json_string_value(name_val) : "unknown"; + char uid_value[128] = {0}; + int width = 0; + int height = 0; if (!has_usable_device_uid(dev)) { @@ -354,22 +548,59 @@ static int search_lcd_device(const json_t *devices, char *lcd_uid, continue; } - extract_lcd_device_info(dev, lcd_uid, uid_size, found_lcd_device, + extract_device_uid(dev, uid_value, sizeof(uid_value)); + extract_lcd_dimensions(dev, &width, &height); + + int score = score_lcd_candidate(config, uid_value, name, type_str, width, + height); + if (score < 0) + { + log_message(LOG_INFO, + "Skipping filtered LCD candidate: %s [%s] (%dx%d)", + name ? name : "unknown", + type_str ? type_str : "unknown type", width, height); + continue; + } + + candidate_count++; + if (score > best_score) + { + best_score = score; + best_dev = dev; + } + + log_message(LOG_INFO, + "LCD candidate score=%d: %s [%s] (%dx%d, uid=%s)", score, + name ? name : "unknown", + type_str ? type_str : "unknown type", width, height, + uid_value[0] != '\0' ? uid_value : "n/a"); + } + + if (best_dev) + { + extract_lcd_device_info(best_dev, lcd_uid, uid_size, found_lcd_device, screen_width, screen_height, device_name, name_size); return 1; } - return 1; + if (candidate_count == 0) + { + log_message(LOG_WARNING, + "No suitable LCD device candidates found after filtering"); + } + + return 0; } /** * @brief Parse devices JSON and extract LCD UID, display info and device name. */ -static int parse_lcd_device_data(const char *json, char *lcd_uid, - size_t uid_size, int *found_lcd_device, - int *screen_width, int *screen_height, - char *device_name, size_t name_size) +static int parse_lcd_device_data(const Config *config, const char *json, + char *lcd_uid, size_t uid_size, + int *found_lcd_device, int *screen_width, + int *screen_height, char *device_name, + size_t name_size) { if (!json) return 0; @@ -393,7 +624,7 @@ static int parse_lcd_device_data(const char *json, char *lcd_uid, return 0; } - int result = search_lcd_device(devices, lcd_uid, uid_size, + int result = search_lcd_device(config, devices, lcd_uid, uid_size, found_lcd_device, screen_width, screen_height, device_name, name_size); json_decref(root); @@ -408,6 +639,7 @@ static void configure_device_cache_curl(CURL *curl, const Config *config, http_response *chunk, struct curl_slist **headers) { + (void)config; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, @@ -417,22 +649,11 @@ static void configure_device_cache_curl(CURL *curl, const Config *config, *headers = curl_slist_append(NULL, "accept: application/json"); - /* Attach auth: Bearer token preferred, otherwise share session cookie */ const char *bearer = get_session_access_token(); if (bearer && bearer[0] != '\0') - { *headers = curl_slist_append(*headers, bearer); - } - else - { - const char *cookie_jar = get_session_cookie_jar(); - if (cookie_jar && cookie_jar[0] != '\0') - curl_easy_setopt(curl, CURLOPT_COOKIEFILE, cookie_jar); - } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers); - - apply_ssl_options(curl, config); } /** @@ -501,14 +722,16 @@ static void populate_device_name_cache(const char *json_data) /** * @brief Process device cache API response and populate cache. */ -static int process_device_cache_response(const http_response *chunk) +static int process_device_cache_response(const Config *config, + const http_response *chunk) { /* Populate device name cache for ALL devices (used by sensor system) */ populate_device_name_cache(chunk->data); int found_lcd_device = 0; int result = parse_lcd_device_data( - chunk->data, device_cache.device_uid, sizeof(device_cache.device_uid), + config, chunk->data, device_cache.device_uid, + sizeof(device_cache.device_uid), &found_lcd_device, &device_cache.screen_width, &device_cache.screen_height, device_cache.device_name, sizeof(device_cache.device_name)); @@ -571,7 +794,7 @@ static int initialize_device_cache(const Config *config) int success = 0; if (curl_easy_perform(curl) == CURLE_OK) { - success = process_device_cache_response(&chunk); + success = process_device_cache_response(config, &chunk); } free(chunk.data); @@ -642,6 +865,14 @@ static int update_dimension(uint16_t *config_dim, int device_dim, { const uint16_t original_value = *config_dim; + if (device_dim <= 0) + { + log_message(LOG_WARNING, + "Display %s from device is invalid: %d, keeping config value %u", + dim_name, device_dim, original_value); + return 0; + } + if (*config_dim == 0) { *config_dim = (uint16_t)device_dim; @@ -653,8 +884,11 @@ static int update_dimension(uint16_t *config_dim, int device_dim, if (original_value != (uint16_t)device_dim) { - log_message(LOG_INFO, "Display %s from config.json: %d (device reports %d)", - dim_name, *config_dim, device_dim); + *config_dim = (uint16_t)device_dim; + log_message(LOG_INFO, + "Display %s updated from device: %d (config.json had %u)", + dim_name, *config_dim, original_value); + return 1; } else { diff --git a/src/srv/cc_main.c b/src/srv/cc_main.c index cbe6ce4..c15c075 100644 --- a/src/srv/cc_main.c +++ b/src/srv/cc_main.c @@ -84,7 +84,6 @@ void cc_cleanup_response_buffer(http_response *response) typedef struct { CURL *curl_handle; - char cookie_jar[CC_COOKIE_SIZE]; char access_token[CC_BEARER_HEADER_SIZE]; int session_initialized; } CoolerControlSession; @@ -95,7 +94,7 @@ typedef struct * handle and session initialization status. */ static CoolerControlSession cc_session = { - .curl_handle = NULL, .cookie_jar = {0}, .access_token = {0}, .session_initialized = 0}; + .curl_handle = NULL, .access_token = {0}, .session_initialized = 0}; /** * @brief Reallocate response buffer if needed. @@ -153,9 +152,6 @@ size_t write_callback(const void *contents, size_t size, size_t nmemb, return realsize; } -/* Forward declaration — definition is after cleanup_coolercontrol_session() */ -void apply_ssl_options(void *curl_raw, const struct Config *config); - /** * @brief Validate snprintf result for buffer overflow. * @details Checks if snprintf truncated output. @@ -170,139 +166,40 @@ static int validate_snprintf(int written, size_t buffer_size, char *buffer) return 1; } -/** - * @brief Build login URL and credentials for CoolerControl session. - * @details Constructs the login URL and username:password string from config. - */ -static int build_login_credentials(const Config *config, char *login_url, - size_t url_size, char *userpwd, - size_t pwd_size) -{ - int written_url = - snprintf(login_url, url_size, "%s/login", config->daemon_address); - if (!validate_snprintf(written_url, url_size, login_url)) - return 0; - - int written_pwd = - snprintf(userpwd, pwd_size, "CCAdmin:%s", config->daemon_password); - if (!validate_snprintf(written_pwd, pwd_size, userpwd)) - return 0; - - return 1; -} - -/** - * @brief Configure CURL for CoolerControl login request. - * @details Sets up all CURL options including authentication, headers and SSL. - */ -static struct curl_slist *configure_login_curl(CURL *curl, const Config *config, - const char *login_url, - const char *userpwd) -{ - curl_easy_setopt(curl, CURLOPT_URL, login_url); - curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_easy_setopt(curl, CURLOPT_USERPWD, userpwd); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); - - // Set HTTP headers for login request - struct curl_slist *headers = NULL; - headers = curl_slist_append(headers, "User-Agent: CoolerDash/1.0"); - headers = curl_slist_append(headers, "Accept: application/json"); - headers = curl_slist_append(headers, "Content-Type: application/json"); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - // Enable SSL verification for HTTPS - apply_ssl_options(curl, config); - - return headers; -} - -/** - * @brief Check login response status. - * @details Validates CURL result and HTTP response code. - */ -static int is_login_successful(CURLcode res, long response_code) -{ - return (res == CURLE_OK && (response_code == 200 || response_code == 204)); -} - /** * @brief Initializes the CoolerControl session (CURL setup and login). - * @details This function initializes the CURL library, sets up the session - * cookie jar, constructs the login URL and credentials, and performs a login to - * the CoolerControl API. + * @details This function initializes the CURL library and stores the Bearer + * authorization header used for all CoolerControl API requests. */ int init_coolercontrol_session(const Config *config) { - /* --- Bearer Token path: skip login entirely --- */ - if (config->access_token[0] != '\0') - { - int written = snprintf(cc_session.access_token, sizeof(cc_session.access_token), - "Authorization: Bearer %s", config->access_token); - if (written < 0 || (size_t)written >= sizeof(cc_session.access_token)) - cc_session.access_token[sizeof(cc_session.access_token) - 1] = '\0'; - - /* We still need a CURL handle for LCD uploads */ - curl_global_init(CURL_GLOBAL_DEFAULT); - cc_session.curl_handle = curl_easy_init(); - if (!cc_session.curl_handle) - return 0; - - cc_session.session_initialized = 1; - log_message(LOG_STATUS, "Session initialized using Bearer token (CC4)"); - return 1; + if (!config || config->access_token[0] == '\0') + { + log_message(LOG_ERROR, + "CoolerControl access token missing; token-only authentication is required"); + return 0; } - /* --- Basic Auth / Cookie path (CC3 / fallback) --- */ curl_global_init(CURL_GLOBAL_DEFAULT); cc_session.curl_handle = curl_easy_init(); if (!cc_session.curl_handle) return 0; - int written_cookie = - snprintf(cc_session.cookie_jar, sizeof(cc_session.cookie_jar), - "/tmp/coolerdash_cookie_%d.txt", getpid()); - if (written_cookie < 0 || - (size_t)written_cookie >= sizeof(cc_session.cookie_jar)) + int written = snprintf(cc_session.access_token, sizeof(cc_session.access_token), + "Authorization: Bearer %s", config->access_token); + if (!validate_snprintf(written, sizeof(cc_session.access_token), + cc_session.access_token)) { - cc_session.cookie_jar[sizeof(cc_session.cookie_jar) - 1] = '\0'; - } - - curl_easy_setopt(cc_session.curl_handle, CURLOPT_COOKIEJAR, - cc_session.cookie_jar); - curl_easy_setopt(cc_session.curl_handle, CURLOPT_COOKIEFILE, - cc_session.cookie_jar); - - char login_url[CC_URL_SIZE]; - char userpwd[CC_USERPWD_SIZE]; - if (!build_login_credentials(config, login_url, sizeof(login_url), userpwd, - sizeof(userpwd))) + curl_easy_cleanup(cc_session.curl_handle); + cc_session.curl_handle = NULL; + curl_global_cleanup(); + log_message(LOG_ERROR, "Access token header exceeds maximum size"); return 0; - - struct curl_slist *headers = - configure_login_curl(cc_session.curl_handle, config, login_url, userpwd); - - CURLcode res = curl_easy_perform(cc_session.curl_handle); - long response_code = 0; - curl_easy_getinfo(cc_session.curl_handle, CURLINFO_RESPONSE_CODE, - &response_code); - - memset(userpwd, 0, sizeof(userpwd)); - - if (headers) - curl_slist_free_all(headers); - - if (is_login_successful(res, response_code)) - { - cc_session.session_initialized = 1; - return 1; } - log_message(LOG_ERROR, "Login failed: CURL code %d, HTTP code %ld", res, - response_code); - return 0; + cc_session.session_initialized = 1; + log_message(LOG_STATUS, "Session initialized using Bearer token"); + return 1; } /** @@ -314,8 +211,8 @@ int is_session_initialized(void) { return cc_session.session_initialized; } /** * @brief Cleans up and terminates the CoolerControl session. - * @details This function performs cleanup of the CURL handle, removes the - * cookie jar file, and marks the session as uninitialized. + * @details This function performs cleanup of the CURL handle and marks the + * session as uninitialized. */ void cleanup_coolercontrol_session(void) { @@ -335,14 +232,9 @@ void cleanup_coolercontrol_session(void) // Perform global CURL cleanup curl_global_cleanup(); - // Remove cookie jar file - if (unlink(cc_session.cookie_jar) != 0) - { - all_cleaned = 0; - } - // Mark session as uninitialized cc_session.session_initialized = 0; + cc_session.access_token[0] = '\0'; // Set cleanup flag only if all operations succeeded if (all_cleaned) @@ -359,50 +251,6 @@ const char *get_session_access_token(void) return cc_session.access_token; } -/** - * @brief Returns the session cookie jar path. - */ -const char *get_session_cookie_jar(void) -{ - return cc_session.cookie_jar; -} - -/** - * @brief Apply TLS/SSL options to a CURL handle based on config. - * @details Localhost always uses plain HTTP; for HTTPS addresses this sets - * peer/host verification and optional custom CA cert or skip flag. - */ -void apply_ssl_options(void *curl_raw, const struct Config *config) -{ - if (!curl_raw || !config) - return; - - CURL *curl = (CURL *)curl_raw; - - /* Plain HTTP → nothing to do */ - if (strncmp(config->daemon_address, "https://", 8) != 0) - return; - - if (config->tls_skip_verify) - { - log_message(LOG_WARNING, - "TLS peer verification disabled (tls_skip_verify=true) — insecure!"); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - return; - } - - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - - if (config->tls_ca_cert_path[0] != '\0') - { - curl_easy_setopt(curl, CURLOPT_CAINFO, config->tls_ca_cert_path); - log_message(LOG_INFO, "TLS: using custom CA cert: %s", - config->tls_ca_cert_path); - } -} - /** * @brief Add a string field to multipart form with error checking. * @details Helper function to add a named string field to curl_mime form. @@ -541,13 +389,11 @@ static struct curl_slist *configure_lcd_upload_curl(const Config *config, curl_mime *form, http_response *response) { + (void)config; curl_easy_setopt(cc_session.curl_handle, CURLOPT_URL, upload_url); curl_easy_setopt(cc_session.curl_handle, CURLOPT_MIMEPOST, form); curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, "PUT"); - // Enable SSL verification for HTTPS - apply_ssl_options(cc_session.curl_handle, config); - // Set write callback curl_easy_setopt( cc_session.curl_handle, CURLOPT_WRITEFUNCTION, @@ -559,11 +405,7 @@ static struct curl_slist *configure_lcd_upload_curl(const Config *config, headers = curl_slist_append(headers, "User-Agent: CoolerDash/1.0"); headers = curl_slist_append(headers, "Accept: application/json"); - // Attach auth: Bearer token preferred, otherwise rely on session cookie - if (cc_session.access_token[0] != '\0') - { - headers = curl_slist_append(headers, cc_session.access_token); - } + headers = curl_slist_append(headers, cc_session.access_token); curl_easy_setopt(cc_session.curl_handle, CURLOPT_HTTPHEADER, headers); @@ -671,7 +513,7 @@ int send_image_to_lcd(const Config *config, const char *image_path, * @details Called once at daemon startup. Uploads shutdown.png to * PUT /devices/{uid}/settings/lcd/lcd/shutdown-image so CoolerControl * displays it automatically whenever the CC daemon itself stops. - * Gracefully skips if the endpoint is unavailable (CC3 / pre-CC4). + * Gracefully skips if the endpoint is unavailable. */ int register_shutdown_image_with_cc(const Config *config, const char *image_path, diff --git a/src/srv/cc_main.h b/src/srv/cc_main.h index 03e2c88..7a47f51 100644 --- a/src/srv/cc_main.h +++ b/src/srv/cc_main.h @@ -25,10 +25,8 @@ #include "../device/config.h" // Basic constants -#define CC_COOKIE_SIZE 512 #define CC_UID_SIZE 128 #define CC_URL_SIZE 512 -#define CC_USERPWD_SIZE 128 #define CC_BEARER_HEADER_SIZE (CONFIG_MAX_TOKEN_LEN + 32) // Maximum safe allocation size to prevent overflow @@ -87,8 +85,7 @@ int is_session_initialized(void); /** * @brief Cleans up and terminates the CoolerControl session. - * @details Frees all resources and closes the session, including CURL cleanup - * and cookie file removal. + * @details Frees all resources and closes the session. */ void cleanup_coolercontrol_session(void); @@ -98,19 +95,6 @@ void cleanup_coolercontrol_session(void); */ const char *get_session_access_token(void); -/** - * @brief Returns the cookie jar path for session cookie sharing. - * @details Used by cc_conf and cc_sensor when password-auth is active. - */ -const char *get_session_cookie_jar(void); - -/** - * @brief Apply TLS/SSL options to a CURL handle based on config. - * @details Handles VERIFYPEER/VERIFYHOST/CAINFO/skip-verify logic. - * Call this for every CURL handle that may use HTTPS. - */ -void apply_ssl_options(void *curl, const struct Config *config); - /** * @brief Sends an image directly to the LCD of the CoolerControl device. * @details Uploads an image to the LCD display using a multipart HTTP PUT @@ -123,7 +107,7 @@ int send_image_to_lcd(const struct Config *config, const char *image_path, * @brief Register shutdown image with CC4's persistent LCD shutdown endpoint. * @details Called once at daemon startup. CC4 will display the image * automatically whenever the coolercontrold daemon stops. - * Silently skips on 404 (CC3 / pre-CC4 compatibility). + * Silently skips on 404 when the endpoint is unavailable. */ int register_shutdown_image_with_cc(const struct Config *config, const char *image_path, diff --git a/src/srv/cc_sensor.c b/src/srv/cc_sensor.c index 43cacec..a11a7fd 100644 --- a/src/srv/cc_sensor.c +++ b/src/srv/cc_sensor.c @@ -360,25 +360,13 @@ static int get_sensor_data_from_api(const Config *config, configure_status_request(curl, url, &response); - /* SSL options (apply_ssl_options handles http:// as no-op) */ - apply_ssl_options(curl, config); - struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "accept: application/json"); headers = curl_slist_append(headers, "content-type: application/json"); - /* Attach auth: Bearer token preferred, otherwise share session cookie */ const char *bearer = get_session_access_token(); if (bearer && bearer[0] != '\0') - { headers = curl_slist_append(headers, bearer); - } - else - { - const char *cookie_jar = get_session_cookie_jar(); - if (cookie_jar && cookie_jar[0] != '\0') - curl_easy_setopt(curl, CURLOPT_COOKIEFILE, cookie_jar); - } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); @@ -498,6 +486,62 @@ const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data, return NULL; } +/** + * @brief Find channel sensor matching a slot value and category. + * @details Like find_sensor_for_slot() but matches a specific sensor category + * instead of always matching temperature sensors. + */ +const sensor_entry_t *find_channel_sensor_for_slot( + const monitor_sensor_data_t *data, const char *slot_value, + sensor_category_t category) +{ + if (!data || !slot_value || strcmp(slot_value, "none") == 0) + return NULL; + + /* Legacy slot resolution with custom category */ + if (is_legacy_sensor_slot(slot_value)) + { + 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; + + for (int i = 0; i < data->sensor_count; i++) + { + if (data->sensors[i].category == category && + strcmp(data->sensors[i].device_type, target_type) == 0) + { + return &data->sensors[i]; + } + } + return NULL; + } + + /* Dynamic slot: "device_uid:sensor_name" — match by uid + category */ + const char *separator = strchr(slot_value, ':'); + if (!separator || separator == slot_value) + return NULL; + + size_t uid_len = (size_t)(separator - slot_value); + + for (int i = 0; i < data->sensor_count; i++) + { + if (data->sensors[i].category == category && + strlen(data->sensors[i].device_uid) == uid_len && + strncmp(data->sensors[i].device_uid, slot_value, uid_len) == 0) + { + return &data->sensors[i]; + } + } + + return NULL; +} + // ============================================================================ // Public API // ============================================================================ diff --git a/src/srv/cc_sensor.h b/src/srv/cc_sensor.h index 6545875..684c0ae 100644 --- a/src/srv/cc_sensor.h +++ b/src/srv/cc_sensor.h @@ -109,6 +109,20 @@ const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data, */ int is_legacy_sensor_slot(const char *slot_value); +/** + * @brief Find channel sensor matching a slot value and category. + * @details Like find_sensor_for_slot() but matches a specific sensor category + * (e.g. SENSOR_CATEGORY_FREQ, SENSOR_CATEGORY_WATTS, SENSOR_CATEGORY_RPM) + * instead of always matching temperature sensors. + * @param data Current sensor data collection + * @param slot_value Slot configuration value ("cpu", "gpu", "liquid", or "uid:name") + * @param category Sensor category to match + * @return Pointer to matching sensor entry, or NULL if not found + */ +const sensor_entry_t *find_channel_sensor_for_slot( + const monitor_sensor_data_t *data, const char *slot_value, + sensor_category_t category); + // ============================================================================ // Data Retrieval // ============================================================================ From b9dab2c2fda1c2f02dcbecaeaf346a5adad305e4 Mon Sep 17 00:00:00 2001 From: damachin3 Date: Wed, 8 Apr 2026 22:45:42 +0200 Subject: [PATCH 3/4] fix(UI): refresh restart notice after save #151 --- etc/coolercontrol/plugins/coolerdash/ui/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html index d111925..c508bb5 100644 --- a/etc/coolercontrol/plugins/coolerdash/ui/index.html +++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html @@ -1145,7 +1145,7 @@

System Environment

- Note: Changes are applied immediately — the plugin will be restarted automatically after Save or Reset. + Note: After saving, the service must be restarted manually: systemctl restart cc-plugin-coolerdash.service

From 67510edde2c4d636df944a4639794eb5e112c7bc Mon Sep 17 00:00:00 2001 From: damachin3 Date: Wed, 8 Apr 2026 22:56:17 +0200 Subject: [PATCH 4/4] ci: refresh AUR release workflow --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10416e7..0430c21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -505,7 +505,7 @@ jobs: grep -E "^pkgver=|^_commit=|^pkgrel=" $GITHUB_WORKSPACE/aur-deploy/PKGBUILD || echo "⚠️ Warning: grep found no matches" - name: Deploy to AUR - uses: KSXGitHub/github-actions-deploy-aur@a97f56a8425a7a7f3b8c58607f769c69b089cadb # v3 + uses: KSXGitHub/github-actions-deploy-aur@abe8ac26b51011c88be58c8809fd2ac674068ea5 # v4.1.2 with: pkgname: coolerdash-git pkgbuild: ./aur-deploy/PKGBUILD