Archived
1
1
Fork 0

add dynamic (context aware) temperature sources

This commit is contained in:
Josh Lay 2023-04-22 12:56:09 -05:00
parent aba61c6d0d
commit 36a0bca2f0
Signed by: jlay
GPG key ID: B265E45CACAD108A
4 changed files with 83 additions and 36 deletions

View file

@ -2,7 +2,10 @@
Simple TUI _(using [Textual](https://textual.textualize.io/))_ that shows AMD GPU statistics: Simple TUI _(using [Textual](https://textual.textualize.io/))_ that shows AMD GPU statistics:
- GPU Utilization - GPU Utilization
- Temperature - Temperatures _(as applicable)_
- Edge
- Junction
- Memory
- Core clock - Core clock
- Core voltage - Core voltage
- Memory clock - Memory clock

View file

@ -135,52 +135,71 @@ class GPUStats(App):
class MiscDisplay(Static): class MiscDisplay(Static):
"""A widget to display misc. GPU stats.""" """A widget to display misc. GPU stats."""
# construct the misc. stats dict; appended by discovered temperature nodes
# for bringing in the log writing method # used to make a 'reactive' object
misc_stats = reactive({"util_pct": 0, fan_stats = reactive({"fan_rpm": 0,
"temp": 0, "fan_rpm_target": 0})
"fan_rpm": 0, temp_stats = reactive({})
"fan_rpm_target": 0})
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.timer_misc = None self.timer_fan = None
self.timer_temp = None
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Horizontal(Label("Utilization:",), Label("", id="util_pct", classes="statvalue")) for temp_node in temp_files:
yield Horizontal(Label("Temperature:",), Label("", id="temp_c", classes="statvalue")) # capitalize the first letter for display
caption = temp_node[0].upper() + temp_node[1:]
yield Horizontal(Label(f'[bold]{caption}[/] temp:',), Label("", id="temp_" + temp_node, classes="statvalue"))
yield Horizontal(Label("[underline]Current[/] fan RPM:",), Label("", id="fan_rpm", classes="statvalue")) yield Horizontal(Label("[underline]Current[/] fan RPM:",), Label("", id="fan_rpm", classes="statvalue"))
yield Horizontal(Label("[underline]Target[/] fan RPM:",), Label("", id="fan_rpm_target", classes="statvalue")) yield Horizontal(Label("[underline]Target[/] fan RPM:",), Label("", id="fan_rpm_target", classes="statvalue"))
def on_mount(self) -> None: def on_mount(self) -> None:
"""Event handler called when widget is added to the app.""" """Event handler called when widget is added to the app."""
self.timer_misc = self.set_interval(1, self.update_misc_stats) self.timer_fan = self.set_interval(1, self.update_fan_stats)
self.timer_temp = self.set_interval(1, self.update_temp_stats)
def update_misc_stats(self) -> None: def update_fan_stats(self) -> None:
"""Method to update the 'misc' values to current measurements. """Method to update the 'fan' values to current measurements.
Utilization % and temperature (C)
Run by a timer created 'on_mount'""" Run by a timer created 'on_mount'"""
self.misc_stats = { val_update = {
"util_pct": read_stat(src_files['busy_pct']), "fan_rpm": read_stat(src_files['fan_rpm']),
"temp": int(int(read_stat(src_files['temp_c'])) / 1000), "fan_rpm_target": read_stat(src_files['fan_rpm_target'])
"fan_rpm": read_stat(src_files['fan_rpm']),
"fan_rpm_target": read_stat(src_files['fan_rpm_target'])
} }
self.fan_stats = val_update
def watch_misc_stats(self, misc_stats: dict) -> None: def update_temp_stats(self) -> None:
"""Method to update the 'temperature' values to current measurements.
Run by a timer created 'on_mount'"""
val_update = {}
for temp_node, temp_file in temp_files.items():
# iterate through the discovered temperature nodes
# ... updating the dictionary with new stats
_content = f'{int(read_stat(temp_file)) / 1000:.0f}C'
val_update[temp_node] = _content
self.temp_stats = val_update
def watch_fan_stats(self, fan_stats: dict) -> None:
"""Called when the clocks attribute changes. """Called when the clocks attribute changes.
- Updates label values - Updates label values
- Casting inputs to string to avoid type problems w/ int/None""" - Casting inputs to string to avoid type problems w/ int/None"""
self.query_one("#util_pct", Static).update(f"{misc_stats['util_pct']}%") self.query_one("#fan_rpm", Static).update(f"{fan_stats['fan_rpm']}")
self.query_one("#temp_c", Static).update(f"{misc_stats['temp']}C") self.query_one("#fan_rpm_target", Static).update(f"{fan_stats['fan_rpm_target']}")
self.query_one("#fan_rpm", Static).update(f"{misc_stats['fan_rpm']}")
self.query_one("#fan_rpm_target", Static).update(f"{misc_stats['fan_rpm_target']}") def watch_temp_stats(self, temp_stats: dict) -> None:
"""Called when the clocks attribute changes, updates labels"""
for temp_node in temp_files:
# check first if the reactive object has been updated with keys
if temp_node in temp_stats:
stat_dict_item = temp_stats[temp_node]
self.query_one("#temp_" + temp_node, Static).update(stat_dict_item)
class ClockDisplay(Static): class ClockDisplay(Static):
"""A widget to display GPU power stats.""" """A widget to display GPU power stats."""
clocks = reactive({"sclk": 0, "mclk": 0, "core_voltage": 0}) core_vals = reactive({"sclk": 0, "mclk": 0, "voltage": 0, "util_pct": 0})
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -188,32 +207,35 @@ class ClockDisplay(Static):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Horizontal(Label("Core clock:",), Label("", id="clk_core_val", classes="statvalue")) yield Horizontal(Label("Core clock:",), Label("", id="clk_core_val", classes="statvalue"))
yield Horizontal(Label("Utilization:",), Label("", id="util_pct", classes="statvalue"))
yield Horizontal(Label("Core voltage:",), Label("", id="clk_voltage_val", classes="statvalue")) yield Horizontal(Label("Core voltage:",), Label("", id="clk_voltage_val", classes="statvalue"))
yield Horizontal(Label("Memory clock:"), Label("", id="clk_memory_val", classes="statvalue")) yield Horizontal(Label("Memory clock:"), Label("", id="clk_memory_val", classes="statvalue"))
def on_mount(self) -> None: def on_mount(self) -> None:
"""Event handler called when widget is added to the app.""" """Event handler called when widget is added to the app."""
self.timer_clocks = self.set_interval(1, self.update_clocks) self.timer_clocks = self.set_interval(1, self.update_core_vals)
def update_clocks(self) -> None: def update_core_vals(self) -> None:
"""Method to update GPU clock values to the current measurements. """Method to update GPU clock values to the current measurements.
Run by a timer created 'on_mount'""" Run by a timer created 'on_mount'"""
self.clocks = { self.core_vals = {
"sclk": format_frequency(read_stat(src_files['core_clock'])), "sclk": format_frequency(read_stat(src_files['core_clock'])),
"mclk": format_frequency(read_stat(src_files['memory_clock'])), "mclk": format_frequency(read_stat(src_files['memory_clock'])),
"core_voltage": float( "voltage": float(
f"{int(read_stat(src_files['core_voltage'])) / 1000:.2f}" f"{int(read_stat(src_files['core_voltage'])) / 1000:.2f}"
), ),
"util_pct": read_stat(src_files['busy_pct']),
} }
def watch_clocks(self, clocks: dict) -> None: def watch_core_vals(self, core_vals: dict) -> None:
"""Called when the clocks attribute changes """Called when the clocks attribute changes
- Updates label values - Updates label values
- Casting inputs to string to avoid type problems w/ int/None""" - Casting inputs to string to avoid type problems w/ int/None"""
self.query_one("#clk_core_val", Static).update(f"{clocks['sclk']}") self.query_one("#clk_core_val", Static).update(f"{core_vals['sclk']}")
self.query_one("#clk_voltage_val", Static).update(f"{clocks['core_voltage']}V") self.query_one("#util_pct", Static).update(f"{core_vals['util_pct']}%")
self.query_one("#clk_memory_val", Static).update(f"{clocks['mclk']}") self.query_one("#clk_voltage_val", Static).update(f"{core_vals['voltage']}V")
self.query_one("#clk_memory_val", Static).update(f"{core_vals['mclk']}")
class PowerDisplay(Static): class PowerDisplay(Static):
@ -283,5 +305,27 @@ if __name__ == "__main__":
'fan_rpm': path.join(hwmon_dir, "fan1_input"), 'fan_rpm': path.join(hwmon_dir, "fan1_input"),
'fan_rpm_target': path.join(hwmon_dir, "fan1_target"), 'fan_rpm_target': path.join(hwmon_dir, "fan1_target"),
} }
# notes:
# assumptions are made that freq{1,2}_input files are sclk/mclk
# contents of files named freq{1,2}_label can determine this reliably
# similarly:
# 'in0_input' has a peer named 'in0_label'
# should contain 'vddgfx' to indicate core voltage
# TODO: implement ^
#
# determine temperature nodes, construct an empty dict to store them
temp_files = {}
temp_node_labels = glob.glob(path.join(hwmon_dir, "temp*_label"))
for temp_node_label_file in temp_node_labels:
# determine the base node id, eg: temp1
# construct the path to the file that will label it. ie: edge/junction
temp_node_id = path.basename(temp_node_label_file).split('_')[0]
temp_node_value_file = path.join(hwmon_dir, f"{temp_node_id}_input")
with open(temp_node_label_file, 'r', encoding='utf-8') as _node:
temp_node_name = _node.read().strip()
print(f'found temp: {temp_node_name} (id: {temp_node_id})')
# add the node name/type and the corresponding temp file to the dict
temp_files[temp_node_name] = temp_node_value_file
app = GPUStats() app = GPUStats()
app.run() app.run()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -3,8 +3,8 @@ GPUStatsWidget {
grid-size: 3; grid-size: 3;
grid-gutter: 2 4; grid-gutter: 2 4;
background: $boost; background: $boost;
height: 10; height: 11;
margin: 1; /* margin: 1;*/
min-width: 50; min-width: 50;
padding: 1; padding: 1;
} }
@ -12,7 +12,7 @@ GPUStatsWidget {
/* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */ /* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */
.box { .box {
padding: 1; margin: 1;
height: 100%; height: 100%;
/* border: ascii $primary-background;*/ /* border: ascii $primary-background;*/
border: solid $primary-background; border: solid $primary-background;