diff --git a/extension.js b/extension.js
index 87695818..4882e319 100644
--- a/extension.js
+++ b/extension.js
@@ -80,7 +80,7 @@ var VitalsMenuButton = GObject.registerClass({
this);
let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros',
- 'fixed-widths', 'hide-icons', 'unit',
+ 'fixed-widths', 'hide-icons', 'unit', 'coolant-unit',
'memory-measurement', 'include-public-ip', 'network-public-ip-interval',
'network-public-ip-show-flag', 'network-public-ip-provider', 'network-speed-format', 'network-speed-unit', 'storage-measurement',
'include-static-info', 'include-static-gpu-info' ];
diff --git a/helpers/catalog.js b/helpers/catalog.js
index 36ce0a53..2ce876b7 100644
--- a/helpers/catalog.js
+++ b/helpers/catalog.js
@@ -1,8 +1,11 @@
/* Shared sensor catalog for shell and preferences. */
export const sensorCatalog = {
- 'temperature' : { 'icon': 'temperature-symbolic.svg', colorFormats: ['temp'] },
- 'voltage' : { 'icon': 'voltage-symbolic.svg' },
- 'fan' : { 'icon': 'fan-symbolic.svg', colorFormats: ['fan'] },
+ 'temperature' : { 'icon': 'temperature-symbolic.svg', colorFormats: ['temp'], aggregate: true },
+ 'coolant' : { 'icon': 'water-droplet-symbolic.svg', colorFormats: ['temp'], aggregate: true,
+ unitSetting: 'coolant-unit' },
+ 'voltage' : { 'icon': 'voltage-symbolic.svg', aggregate: true },
+ 'fan' : { 'icon': 'fan-symbolic.svg', colorFormats: ['fan'], aggregate: true },
+ 'pump' : { 'icon': 'pump-symbolic.svg', colorFormats: ['fan'], aggregate: true },
'memory' : { 'icon': 'memory-symbolic.svg', colorFormats: ['percent'] },
'processor' : { 'icon': 'cpu-symbolic.svg', colorFormats: ['percent'] },
'system' : { 'icon': 'system-symbolic.svg', colorFormats: ['load'] },
@@ -268,7 +271,14 @@ export function sensorGroupFromType(type) {
let group = (type || '').replace(/-group$/, '');
if (group.startsWith('gpu'))
return 'gpu';
- return group.replace(/#\d+$/, '');
+ group = group.replace(/#\d+$/, '');
+
+ // types may carry a suffix, eg 'network-rx' or 'network-us'. Every catalog
+ // group is a single word, so fall back to the leading segment.
+ if (!(group in sensorCatalog))
+ group = group.split('-')[0];
+
+ return group;
}
export function colorSettingsKeys() {
@@ -276,3 +286,13 @@ export function colorSettingsKeys() {
.filter(group => sensorCatalog[group].colorFormats)
.map(group => `${group}-colors`);
}
+
+// groups discovered from hardware monitors and summarized by a group average
+export function isAggregateGroup(group) {
+ return !!sensorCatalog[group]?.aggregate;
+}
+
+// a group may pick its own temperature unit, eg coolant reads coolant-unit
+export function unitSettingForType(type) {
+ return sensorCatalog[sensorGroupFromType(type)]?.unitSetting ?? 'unit';
+}
diff --git a/icons/gnome/pump-symbolic.svg b/icons/gnome/pump-symbolic.svg
new file mode 100644
index 00000000..a64d0d17
--- /dev/null
+++ b/icons/gnome/pump-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/gnome/water-droplet-symbolic.svg b/icons/gnome/water-droplet-symbolic.svg
new file mode 100644
index 00000000..9d6199a9
--- /dev/null
+++ b/icons/gnome/water-droplet-symbolic.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/icons/original/pump-symbolic.svg b/icons/original/pump-symbolic.svg
new file mode 100644
index 00000000..a64d0d17
--- /dev/null
+++ b/icons/original/pump-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/original/water-droplet-symbolic.svg b/icons/original/water-droplet-symbolic.svg
new file mode 100644
index 00000000..9d6199a9
--- /dev/null
+++ b/icons/original/water-droplet-symbolic.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/prefs.js b/prefs.js
index 341194fb..107cda1d 100644
--- a/prefs.js
+++ b/prefs.js
@@ -150,8 +150,10 @@ const Settings = new GObject.Class({
this._sensorPageGates = {
'temperature': { toggle: 'show-temperature', widgets: ['unit'] },
+ 'coolant': { toggle: 'show-coolant', widgets: ['coolant-unit'] },
'voltage': { toggle: 'show-voltage', widgets: [] },
'fan': { toggle: 'show-fan', widgets: [] },
+ 'pump': { toggle: 'show-pump', widgets: [] },
'memory': { toggle: 'show-memory', widgets: ['memory-measurement'] },
'processor': { toggle: 'show-processor', widgets: ['include-static-info'] },
'system': { toggle: 'show-system', widgets: ['monitor-cmd'] },
@@ -237,6 +239,7 @@ const Settings = new GObject.Class({
// process sensor toggles
let sensors = [ 'show-temperature', 'show-voltage', 'show-fan',
+ 'show-coolant', 'show-pump',
'show-memory', 'show-processor', 'show-system',
'show-network', 'show-storage', 'use-higher-precision',
'alphabetize', 'hide-zeros', 'include-public-ip',
@@ -264,7 +267,7 @@ const Settings = new GObject.Class({
// process individual drop down sensor preferences
sensors = [
- 'position-in-panel', 'unit', 'network-speed-format', 'network-speed-unit',
+ 'position-in-panel', 'unit', 'coolant-unit', 'network-speed-format', 'network-speed-unit',
'memory-measurement', 'storage-measurement', 'battery-slot', 'icon-style',
'network-public-ip-provider'
];
diff --git a/prefs.ui b/prefs.ui
index c8d1c19b..098e3c91 100644
--- a/prefs.ui
+++ b/prefs.ui
@@ -202,6 +202,45 @@
+
+
+
voltage
@@ -250,6 +289,30 @@
+
+
+ pump
+ Pump
+ pump-symbolic
+
+
+ 10
+ 10
+
+
+ Monitor pump
+ show-pump
+
+
+ center
+
+
+
+
+
+
+
+
memory
diff --git a/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/schemas/org.gnome.shell.extensions.vitals.gschema.xml
index 3bb9fa5d..6e523479 100644
--- a/schemas/org.gnome.shell.extensions.vitals.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.vitals.gschema.xml
@@ -41,11 +41,26 @@
Temperature unit
The unit ('centigrade' or 'fahrenheit') the extension should display the temperature in
+
+ 0
+ Coolant temperature unit
+ The unit ('centigrade' or 'fahrenheit') the extension should display the coolant temperature in
+
true
Monitor voltage
Display voltage of various components
+
+ true
+ Monitor coolant
+ Display liquid cooling loop coolant temperature
+
+
+ true
+ Monitor pump
+ Display liquid cooling pump rotation per minute
+
true
Monitor fan
@@ -179,6 +194,16 @@
Fan color thresholds
Color rules matched against fan RPM. Entries are "threshold r g b".
+
+ ["35 0.9647058844566345 0.8274509906768799 0.1764705926179886", "40 1 0.47058823704719543 0", "45 0.8784313797950745 0.10588235408067703 0.1411764770746231"]
+ Coolant color thresholds
+ Color rules matched against the displayed coolant temperature. Entries are "threshold r g b".
+
+
+ []
+ Pump color thresholds
+ Color rules matched against pump RPM. Entries are "threshold r g b".
+
["65 0.9647058844566345 0.8274509906768799 0.1764705926179886", "80 1 0.47058823704719543 0", "92 0.8784313797950745 0.10588235408067703 0.1411764770746231"]
Memory usage color thresholds
diff --git a/sensors.js b/sensors.js
index 8c303931..5da28130 100644
--- a/sensors.js
+++ b/sensors.js
@@ -26,8 +26,10 @@
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
+import Gio from 'gi://Gio';
import * as SubProcessModule from './helpers/subprocess.js';
import * as FileModule from './helpers/file.js';
+import { isAggregateGroup } from './helpers/catalog.js';
import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
let GTop, hasGTop = true;
@@ -134,8 +136,8 @@ export const Sensors = GObject.registerClass({
for (let sensor in this._sensorIcons) {
if (this._settings.get_boolean('show-' + sensor)) {
- if (sensor == 'temperature' || sensor == 'voltage' || sensor == 'fan') {
- // for temp, volt, fan, we have a shared handler
+ if (isAggregateGroup(sensor)) {
+ // hardware monitor sensors share a handler
this._queryTempVoltFan(callback, sensor);
} else {
// directly call queryFunction below
@@ -150,6 +152,11 @@ export const Sensors = GObject.registerClass({
for (let label in this._tempVoltFanSensors[type]) {
let sensor = this._tempVoltFanSensors[type][label];
+ if (sensor['aquacomputer']) {
+ this._readAquacomputerSensor(callback, label, sensor, type);
+ continue;
+ }
+
new FileModule.File(sensor['path']).read().then(value => {
this._returnValue(callback, label, value, type, sensor['format']);
}).catch(err => {
@@ -158,6 +165,53 @@ export const Sensors = GObject.registerClass({
}
}
+ // The Aquacomputer Vision publishes a 64 byte HID report rather than hwmon
+ // entries. Byte 0 is the report id and offset 0x37 holds the coolant
+ // temperature as a big endian value in hundredths of a degree.
+ _readAquacomputerSensor(callback, label, sensor, type) {
+ const REPORT_LENGTH = 64;
+ const REPORT_ID = 0x01;
+ const COOLANT_TEMP_OFFSET = 0x37;
+
+ let disabled = () => this._returnValue(callback, label, 'disabled', type, sensor['format']);
+
+ try {
+ Gio.File.new_for_path(sensor['path']).read_async(GLib.PRIORITY_DEFAULT, null, (file, res) => {
+ let stream;
+
+ try {
+ stream = file.read_finish(res);
+ } catch (e) {
+ disabled();
+ return;
+ }
+
+ stream.read_bytes_async(REPORT_LENGTH, GLib.PRIORITY_DEFAULT, null, (input, res2) => {
+ try {
+ let data = new Uint8Array(input.read_bytes_finish(res2).get_data());
+
+ if (data.length < REPORT_LENGTH || data[0] !== REPORT_ID) {
+ disabled();
+ return;
+ }
+
+ let raw = (data[COOLANT_TEMP_OFFSET] << 8) | data[COOLANT_TEMP_OFFSET + 1];
+
+ // hundredths of a degree to millidegrees
+ this._returnValue(callback, label, raw * 10, type, sensor['format']);
+ } catch (e) {
+ disabled();
+ } finally {
+ // the device is polled on every refresh, so don't leak the fd
+ input.close_async(GLib.PRIORITY_DEFAULT, null, null);
+ }
+ });
+ });
+ } catch (e) {
+ disabled();
+ }
+ }
+
_queryMemory(callback) {
// check memory info
new FileModule.File('/proc/meminfo').read().then(lines => {
@@ -802,7 +856,8 @@ export const Sensors = GObject.registerClass({
}
_discoverHardwareMonitors(callback) {
- this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {} };
+ this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {},
+ 'coolant': {}, 'pump': {} };
let hwbase = '/sys/class/hwmon/';
@@ -815,7 +870,8 @@ export const Sensors = GObject.registerClass({
if (this._settings.get_boolean('show-voltage'))
sensor_types['in'] = 'voltage';
- if (this._settings.get_boolean('show-fan'))
+ // the pump reports through a fan input, so discover fans for either group
+ if (this._settings.get_boolean('show-fan') || this._settings.get_boolean('show-pump'))
sensor_types['fan'] = 'fan';
// a little informal, but this code has zero I/O block
@@ -891,6 +947,79 @@ export const Sensors = GObject.registerClass({
this._reconfigureNvidiaSmiProcess();
this._discoverGpuDrm();
this._initFrameMonitor();
+ this._discoverAquacomputerVision();
+ }
+
+ // The Aquacomputer Vision has no hwmon driver, so find its raw HID node by
+ // walking /sys/class/hidraw and matching the USB vendor and product id.
+ // It is a composite device that also exposes keyboard and consumer control
+ // interfaces, and only the vendor defined one carries telemetry, so match
+ // the report descriptor's usage page too. Reading the wrong interface would
+ // block forever because it never sends a report.
+ _discoverAquacomputerVision() {
+ const VENDOR_ID = '0c70';
+ const PRODUCT_ID = 'f00c';
+ const HIDRAW_CLASS = '/sys/class/hidraw/';
+
+ // Usage Page (Vendor Defined 0xFF00)
+ const VENDOR_USAGE_PAGE = [ 0x06, 0x00, 0xff ];
+
+ let enumerator;
+
+ try {
+ enumerator = Gio.File.new_for_path(HIDRAW_CLASS).enumerate_children(
+ 'standard::name', Gio.FileQueryInfoFlags.NONE, null);
+ } catch (e) {
+ // no hidraw devices present, or we can't enumerate them
+ return;
+ }
+
+ try {
+ let info;
+ while ((info = enumerator.next_file(null)) !== null) {
+ let name = info.get_name();
+ let device;
+
+ try {
+ // resolves to something like ../../devices/.../0003:0C70:F00C.000B
+ device = GLib.file_read_link(HIDRAW_CLASS + name + '/device');
+ } catch (e) {
+ continue;
+ }
+
+ let ids = device.substr(device.lastIndexOf('/') + 1)
+ .match(/^[0-9A-Fa-f]+:([0-9A-Fa-f]{4}):([0-9A-Fa-f]{4})\./);
+
+ if (!ids || ids[1].toLowerCase() != VENDOR_ID || ids[2].toLowerCase() != PRODUCT_ID)
+ continue;
+
+ let descriptor;
+
+ try {
+ let [ok, contents] = GLib.file_get_contents(
+ HIDRAW_CLASS + name + '/device/report_descriptor');
+
+ if (!ok) continue;
+ descriptor = contents;
+ } catch (e) {
+ continue;
+ }
+
+ if (!VENDOR_USAGE_PAGE.every((byte, i) => descriptor[i] === byte))
+ continue;
+
+ this._addTempVoltFan(null, {
+ 'type': 'coolant',
+ 'format': 'temp',
+ 'input': '/dev/' + name,
+ 'aquacomputer': true
+ }, 'AC Vision', 'Coolant Temp', '', 0);
+
+ break;
+ }
+ } finally {
+ enumerator.close(null);
+ }
}
_discoverGpuDrm() {
@@ -1045,18 +1174,26 @@ export const Sensors = GObject.registerClass({
if (label == 'iwlwifi_1 temp1') label = 'Wireless Adapter';
if (label == 'Package id 0') label = 'Processor 0';
if (label == 'Package id 1') label = 'Processor 1';
+ if (label == 'nct6799 fan1') label = 'VRM HeatSink Fan';
+ if (label == 'nct6799 fan2') label = 'Radiator Fan(s)';
+ if (label == 'nct6799 fan6') label = 'Chipset/NVMe Fan';
+ if (label == 'nct6799 fan7') label = 'AIO Pump';
+ if (label == 'nct6799 SYSTIN') label = 'Motherboard Temp';
+ if (label == 'nct6799 CPUTIN') label = 'CPU Socket Temp';
label = label.replace('Package id', 'CPU');
- let types = [ 'temperature', 'voltage', 'fan' ];
- for (let type of types) {
+ // the pump arrives on a fan input but belongs to its own group
+ let sensorType = (label == 'AIO Pump') ? 'pump' : obj['type'];
+
+ for (let group in this._tempVoltFanSensors) {
// check if this label already exists
- if (label in this._tempVoltFanSensors[type]) {
+ if (label in this._tempVoltFanSensors[group]) {
for (let i = 2; i <= 9; i++) {
// append an incremented number to end
let new_label = label + ' ' + i;
// if new label is available, use it
- if (!(new_label in this._tempVoltFanSensors[type])) {
+ if (!(new_label in this._tempVoltFanSensors[group])) {
label = new_label;
break;
}
@@ -1065,11 +1202,14 @@ export const Sensors = GObject.registerClass({
}
// update screen on initial build to prevent delay on update
- this._returnValue(callback, label, value, obj['type'], obj['format']);
-
- this._tempVoltFanSensors[obj['type']][label] = {
- 'format': obj['format'],
- 'path': obj['input']
+ // raw HID sensors are registered without a value, so skip them here
+ if (callback && !obj['aquacomputer'])
+ this._returnValue(callback, label, value, sensorType, obj['format']);
+
+ this._tempVoltFanSensors[sensorType][label] = {
+ 'format': obj['format'],
+ 'path': obj['input'],
+ 'aquacomputer': obj['aquacomputer'] || false
};
}
diff --git a/stylesheet.css b/stylesheet.css
index 17879c21..63853699 100644
--- a/stylesheet.css
+++ b/stylesheet.css
@@ -1,8 +1,10 @@
.vitals-panel-item{spacing: 0;}
.vitals-panel-menu{spacing: 11px; padding: 3px; }
.vitals-panel-icon-temperature { margin: 0 1px 0 0; padding: 0; }
+.vitals-panel-icon-coolant { margin: 0 1px 0 0; padding: 0; }
.vitals-panel-icon-voltage { margin: 0 0 0 0; padding: 0; }
.vitals-panel-icon-fan { margin: 0 4px 0 0; padding: 0; }
+.vitals-panel-icon-pump { margin: 0 4px 0 0; padding: 0; }
.vitals-panel-icon-memory { margin: 0 2px 0 0; padding: 0; }
.vitals-panel-icon-processor { margin: 0 3px 0 0; padding: 0; }
.vitals-panel-icon-system { margin: 0 3px 0 0; padding: 0; }
diff --git a/values.js b/values.js
index 1255e8ce..388391f2 100644
--- a/values.js
+++ b/values.js
@@ -26,7 +26,7 @@
import GObject from 'gi://GObject';
-import {sensorCatalog, sensorGroupFromType} from './helpers/catalog.js';
+import {sensorCatalog, sensorGroupFromType, isAggregateGroup, unitSettingForType} from './helpers/catalog.js';
const cbFun = (d, c) => {
let bb = d[1] % c[0],
@@ -94,15 +94,15 @@ function getUsageColor(value, colors) {
}
function colorsKeyForSensor(type, format) {
- // All temperatures share the temperature threshold UI, including GPU rows.
- if (format === 'temp')
- return 'temperature-colors';
-
const group = sensorGroupFromType(type);
const formats = sensorCatalog[group]?.colorFormats;
if (formats && formats.includes(format))
return `${group}-colors`;
+ // remaining temperatures share the temperature threshold UI, including GPU rows
+ if (format === 'temp')
+ return 'temperature-colors';
+
return null;
}
@@ -150,7 +150,7 @@ export const Values = GObject.registerClass({
ending = '°C';
// are we converting to fahrenheit?
- if (this._settings.get_int('unit') == 1) {
+ if (this._settings.get_int(unitSettingForType(type)) == 1) {
value = ((9 / 5) * value + 32);
ending = '°F';
}
@@ -346,7 +346,7 @@ export const Values = GObject.registerClass({
this._history[historyType][key] = [legible.text, value];
// process average, min and max values
- if (type == 'temperature' || type == 'voltage' || type == 'fan') {
+ if (isAggregateGroup(type)) {
let vals = Object.values(this._history[type]).map(x => parseFloat(x[1]));
// show value in group even if there is one value present