amdgpu_fan 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f831dbba03b1f54f0ed55055eea94371b658b4e797dae7899f3954b55e755a28
4
- data.tar.gz: 9fa6dd181fbcae74b6441fe9a62200fb2be98be59c88d470a434c2b2730ea691
3
+ metadata.gz: 822ba3e84cdd6cb64680ace91e696351c3ac8f61fe7344554304be517c1ee141
4
+ data.tar.gz: b74858f3916aec38afb02134694011f59f4c672d276dd4905e9da6accee69333
5
5
  SHA512:
6
- metadata.gz: 92a2ab4778b838e1bbbe6607c8bb2ad4a5d9ec79f817f24fa03d5af460e73a857e2dd456b3fd0ca08ab58cc4a64924a981afd4a44c05be65dad449b65f56c1e9
7
- data.tar.gz: 4b4d0cd42f4a0275e09230799c22c90eecf556a0adfa2918d579f43419878a2100f2230cb19491468b59dc1496badc71bfd9f6332c6fddb3d7cc7480f4c00b97
6
+ metadata.gz: 1798fa2cb6a578717c75d898501833e75e27fc8efd14881954c8a7cdf53e71a0dc1dfb0cba6b51b8b96572943761d6d9f23be33f26f8bc6ee3c42e38f017ab7c
7
+ data.tar.gz: 95df0c6e478a1cbe580695c9be8cf123e555ec76ef767eb5a91b24d174693667cf33b0dabcc6a3ddd45cdfd54a7efd070317e6a74fc63395f2b96d207b77e0eb
data/README.md CHANGED
@@ -1,10 +1,16 @@
1
- # amdgpu-fan-rb
1
+ # amdgpu_fan
2
2
 
3
3
  [![Build Status](https://travis-ci.org/HarlemSquirrel/amdgpu-fan-rb.svg?branch=master)](https://travis-ci.org/HarlemSquirrel/amdgpu-fan-rb) [![Maintainability](https://api.codeclimate.com/v1/badges/27233cee17ef6a2c14fd/maintainability)](https://codeclimate.com/github/HarlemSquirrel/amdgpu-fan-rb/maintainability)
4
4
 
5
- A Ruby CLI to read and set the fan speed for AMD Radeon graphics cards running on the AMDGPU Linux driver.
5
+ A Ruby CLI to read and set fan speed, power profiles, and more for AMD Radeon graphics cards running on the AMDGPU Linux driver.
6
6
 
7
- See https://wiki.archlinux.org/index.php/Fan_speed_control#AMDGPU_sysfs_fan_control
7
+ **amdgpu_fan** aims to provide a more user friendly interface on top of [sysfs](https://en.wikipedia.org/wiki/Sysfs) for displaying statistics and interacting with AMD Radeon graphics hardware running on the [AMDgpu](https://dri.freedesktop.org/docs/drm/gpu/amdgpu.html) driver.
8
+
9
+ #### Further reading
10
+
11
+ - https://wiki.archlinux.org/index.php/AMDGPU#Overclocking
12
+ - https://wiki.archlinux.org/index.php/Fan_speed_control#AMDGPU_sysfs_fan_control
13
+ - https://phoronix.com/scan.php?page=news_item&px=AMDGPU-Quick-WattMan-Cap-Test
8
14
 
9
15
  ## Installation
10
16
 
@@ -28,25 +34,28 @@ gem install amdgpu_fan
28
34
  ## Usage
29
35
 
30
36
  ```
31
- ➤ bin/amdgpu_fan
37
+ ➤ bin/amdgpu_fan help
32
38
  Commands:
33
- amdgpu_fan auto # Set fan mode to automatic (requires sudo)
34
- amdgpu_fan connectors # View the status of the display connectors
35
- amdgpu_fan help [COMMAND] # Describe available commands or one specific command
36
- amdgpu_fan profile # View power profile details.
37
- amdgpu_fan profile_auto # Set the power profile to automatic mode.
38
- amdgpu_fan profile_force [PROFILE_NUM] # Manually set a power profile.
39
- amdgpu_fan set PERCENTAGE # Set fan speed to PERCENTAGE (requires sudo)
40
- amdgpu_fan status [--logo] # View device info, current fan speed, and temperature
41
- amdgpu_fan watch [SECONDS] # Watch fan speed, load, power, and temperature refreshed every n seconds
42
- amdgpu_fan watch_csv [SECONDS] # Watch stats in CSV format refreshed every n seconds defaulting to 1 second
43
-
44
- ➤ amdgpu_fan status
45
- 📺 GPU: AMD Radeon (TM) R9 Fury Series
46
- 📄 vBIOS: 113-C8800100-102
47
- 🌀 Fan: auto mode running at 48% ~ 1828 rpm
48
- 🌡 Temp: 28.0°C
49
- ⚡ Power: 19.26 / 300.0 Watts
39
+ amdgpu_fan connectors # View the status of the display connectors.
40
+ amdgpu_fan fan # View fan details.
41
+ amdgpu_fan fan_set PERCENTAGE/AUTO # Set fan speed to percentage or automatic mode. (requires sudo)
42
+ amdgpu_fan help [COMMAND] # Describe available commands or one specific command
43
+ amdgpu_fan profile # View power profile details.
44
+ amdgpu_fan profile_auto # Set the power profile to automatic mode.
45
+ amdgpu_fan profile_force PROFILE_NUM # Manually set a power profile. (requires sudo)
46
+ amdgpu_fan status [--logo] # View device info, current fan speed, and temperature.
47
+ amdgpu_fan watch [SECONDS] # Watch fan speed, load, power, and temperature refreshed every n seconds.
48
+ amdgpu_fan watch_csv [SECONDS] # Watch stats in CSV format refreshed every n seconds defaulting to 1 second.
49
+
50
+ bin/amdgpu_fan status
51
+ 📺 GPU: Advanced Micro Devices, Inc. [AMD/ATI] Radeon R9 FURY X / NANO
52
+ 📄 vBIOS: 113-C8800100-102
53
+ ⏰ Clocks: 724Mhz Core, 500Mhz Memory
54
+ 💾 Memory: 4096 MiB
55
+ 🌀 Fan: auto mode running at 1809 rpm (48%)
56
+ 🌞 Temp: 21.0°C
57
+ ⚡ Power: 3D_FULL_SCREEN profile in performance mode using 16.2 / 300.0 Watts (5%)
58
+ ⚖ Load: [ ]0%
50
59
 
51
60
  ➤ bin/amdgpu_fan watch 3
52
61
  Watching Advanced Micro Devices, Inc. [AMD/ATI] Radeon R9 FURY X / NANO every 3 second(s)...
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../lib/amdgpu_fan_cli'
4
+ require_relative '../lib/amdgpu_fan'
5
5
 
6
- AmdgpuFanCli.start ARGV
6
+ AmdgpuFan::Cli.start ARGV
@@ -2,4 +2,10 @@
2
2
 
3
3
  require 'thor'
4
4
 
5
- require_relative '../lib/amdgpu_service'
5
+ require_relative '../lib/amdgpu_fan'
6
+
7
+ require_relative '../lib/amdgpu_fan/mixin/cli_output_format'
8
+ require_relative '../lib/amdgpu_fan/mixin/sys_write'
9
+
10
+ require_relative '../lib/amdgpu_fan/service'
11
+ require_relative '../lib/amdgpu_fan/cli'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../config/environment'
4
+
5
+ # The main module
6
+ module AmdgpuFan
7
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # The command-line interface class
5
+ class Cli < Thor
6
+ include CliOutputFormat
7
+
8
+ WATCH_FIELD_SEPARATOR = ' | '
9
+
10
+ desc 'connectors', 'View the status of the display connectors.'
11
+ def connectors
12
+ amdgpu_service.connectors.each do |connector|
13
+ puts "#{connector.type} #{connector.index}:\t" +
14
+ (connector.connected? ? connector.display_name : connector.status)
15
+ end
16
+ end
17
+
18
+ desc 'profile', 'View power profile details.'
19
+ def profile
20
+ puts amdgpu_service.profile_summary
21
+ end
22
+
23
+ desc 'profile_auto', 'Set the power profile to automatic mode.'
24
+ def profile_auto
25
+ amdgpu_service.profile_auto
26
+ puts amdgpu_service.profile_summary
27
+ end
28
+
29
+ desc 'profile_force PROFILE_NUM', 'Manually set a power profile. (requires sudo)'
30
+ def profile_force(state)
31
+ amdgpu_service.profile_force = state
32
+ puts amdgpu_service.profile_summary
33
+ end
34
+
35
+ desc 'fan', 'View fan details.'
36
+ def fan
37
+ puts fan_status
38
+ end
39
+
40
+ desc 'fan_set PERCENTAGE/AUTO', 'Set fan speed to percentage or automatic mode. (requires sudo)'
41
+ def fan_set(value)
42
+ if value.strip.casecmp('auto').zero?
43
+ amdgpu_service.fan_mode = :auto
44
+ else
45
+ return puts 'Invalid percentage' unless (0..100).cover?(value.to_i)
46
+
47
+ amdgpu_service.fan_speed = value
48
+ end
49
+ puts fan_status
50
+ end
51
+
52
+ desc 'status [--logo]', 'View device info, current fan speed, and temperature.'
53
+ def status(option = nil)
54
+ puts radeon_logo if option == '--logo'
55
+ puts "👾 #{'GPU:'.ljust(9)} #{amdgpu_service.name}",
56
+ "📄 #{'vBIOS:'.ljust(9)} #{amdgpu_service.vbios_version}",
57
+ "📺 Displays: #{amdgpu_service.connectors.map(&:display_name).compact.join(',')}",
58
+ "⏰ #{'Clocks:'.ljust(9)} #{clock_status}",
59
+ "💾 #{'Memory:'.ljust(9)} #{mem_total_mibibyes}",
60
+ "🌀 #{'Fan:'.ljust(9)} #{fan_status}",
61
+ "🌞 #{'Temp:'.ljust(9)} #{amdgpu_service.temperature}°C",
62
+ "⚡ #{'Power:'.ljust(9)} #{amdgpu_service.profile_mode} profile in " \
63
+ "#{amdgpu_service.power_dpm_state} mode using " \
64
+ "#{amdgpu_service.power_draw} / #{amdgpu_service.power_max} Watts "\
65
+ "(#{amdgpu_service.power_draw_percent}%)",
66
+ "⚖ #{'Load:'.ljust(9)} #{percent_meter amdgpu_service.busy_percent, 20}"
67
+ end
68
+
69
+ desc 'watch [SECONDS]', 'Watch fan speed, load, power, and temperature ' \
70
+ 'refreshed every n seconds.'
71
+ def watch(seconds = 1)
72
+ return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
73
+
74
+ puts "Watching #{amdgpu_service.name} every #{seconds} second(s)...",
75
+ ' <Press Ctrl-C to exit>'
76
+
77
+ trap 'SIGINT' do
78
+ puts "\nAnd now the watch is ended."
79
+ exit 0
80
+ end
81
+
82
+ loop do
83
+ time = Time.now
84
+ puts [time.strftime('%F %T'), summary_clock, summary_fan, summary_load, summary_power,
85
+ summary_temp].join(WATCH_FIELD_SEPARATOR)
86
+
87
+ # It can take a second or two to run the above so we remove them from the wait
88
+ # here to get a more consistant watch interval.
89
+ sec_left_to_wait = time.to_i + seconds.to_i - Time.now.to_i
90
+ sleep sec_left_to_wait if sec_left_to_wait.positive?
91
+ end
92
+ end
93
+
94
+ desc 'watch_csv [SECONDS]', 'Watch stats in CSV format ' \
95
+ 'refreshed every n seconds defaulting to 1 second.'
96
+ def watch_csv(seconds = 1)
97
+ return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
98
+
99
+ puts 'Timestamp, Core Clock (Mhz),Memory Clock (Mhz),Fan speed (rpm), '\
100
+ 'Load (%),Power (Watts),Temp (°C)'
101
+
102
+ trap 'SIGINT' do
103
+ exit 0
104
+ end
105
+
106
+ loop do
107
+ puts [Time.now.strftime('%F %T'),
108
+ amdgpu_service.core_clock,
109
+ amdgpu_service.memory_clock,
110
+ amdgpu_service.fan_speed_rpm,
111
+ amdgpu_service.busy_percent,
112
+ amdgpu_service.power_draw,
113
+ amdgpu_service.temperature].join(',')
114
+ sleep seconds.to_i
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def amdgpu_service
121
+ @amdgpu_service ||= AmdgpuFan::Service.new
122
+ end
123
+
124
+ def clock_status
125
+ "#{amdgpu_service.core_clock} Core, #{amdgpu_service.memory_clock} Memory"
126
+ end
127
+
128
+ def fan_status
129
+ "#{amdgpu_service.fan_mode} mode running at " \
130
+ "#{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%)"
131
+ end
132
+
133
+ def mem_total_mibibyes
134
+ "#{amdgpu_service.memory_total / (2**20)} MiB"
135
+ end
136
+
137
+ def power_max
138
+ format('%<num>0.2f', num: amdgpu_service.power_max)
139
+ end
140
+
141
+ def summary_clock
142
+ "Core: #{amdgpu_service.core_clock.rjust(7)}#{WATCH_FIELD_SEPARATOR}"\
143
+ "Memory: #{amdgpu_service.memory_clock.rjust(7)}"
144
+ end
145
+
146
+ def summary_fan
147
+ fan_speed_string = "#{amdgpu_service.fan_speed_rpm} rpm".rjust(8)
148
+ "Fan: #{fan_speed_string} #{percent_meter(amdgpu_service.fan_speed_percent)}"
149
+ end
150
+
151
+ def summary_load
152
+ "Load: #{percent_meter amdgpu_service.busy_percent}"
153
+ end
154
+
155
+ def summary_power
156
+ "Power: #{format('%<num>0.02f', num: amdgpu_service.power_draw).rjust(power_max.length)} W" \
157
+ " #{percent_meter amdgpu_service.power_draw_percent}"
158
+ end
159
+
160
+ def summary_temp
161
+ temp_string = "#{amdgpu_service.temperature}°C".rjust(7)
162
+ "Temp: #{temp_string}"
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ ## Connector
5
+ #
6
+ # A model class for a GPU connector
7
+ class Connector
8
+ EDID_DESCRIPTORS_CONF = {
9
+ display_descriptor_leading_bytes: String.new('\x00\xFC\x00', encoding: 'ascii-8bit'),
10
+ index_range: (54..125)
11
+ }.freeze
12
+
13
+
14
+ attr_reader :card_num, :dir_path, :index, :type
15
+
16
+ class << self
17
+ ##
18
+ # Return an array of connector objects for the provided card number.
19
+ # The files are sorted to improve how they are displayed to the user.
20
+ def where(card_num:)
21
+ Dir["/sys/class/drm/card#{card_num}/card#{card_num}-*"].sort.map do |dir_path|
22
+ Connector.new card_num: card_num,
23
+ dir_path: dir_path,
24
+ index: dir_path[-1],
25
+ type: dir_path.slice(/(?<=card#{card_num}-)[A-Z]+/)
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(card_num:, dir_path:, index:, type:)
31
+ @card_num = card_num
32
+ @dir_path = dir_path
33
+ @index = index
34
+ @type = type
35
+ end
36
+
37
+ def connected?
38
+ status.casecmp('connected').zero?
39
+ end
40
+
41
+ def display_name
42
+ return if edid.to_s.empty?
43
+
44
+ edid.slice(EDID_DESCRIPTORS_CONF[:index_range])
45
+ .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:display_descriptor_leading_bytes]}).{1,13}/)
46
+ .first
47
+ .strip
48
+ end
49
+
50
+ def status
51
+ File.read(File.join(dir_path, 'status')).strip
52
+ end
53
+
54
+ private
55
+
56
+ def edid
57
+ File.read("#{dir_path}/edid", encoding: 'ascii-8bit')
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # A mixin to help with CLI output formatting
5
+ module CliOutputFormat
6
+ METER_CHAR = '*'
7
+ TIME_FORMAT = '%F %T'
8
+
9
+ private
10
+
11
+ def current_time
12
+ Time.now.strftime(TIME_FORMAT)
13
+ end
14
+
15
+ def percent_meter(percent, length = 10)
16
+ progress_bar_count = (length * percent.to_f / 100).round
17
+ percent_string = "#{format '%<num>0.2i', num: percent}%".ljust(3)
18
+ "[#{METER_CHAR * progress_bar_count}#{' ' * (length - progress_bar_count)}]#{percent_string}"
19
+ end
20
+
21
+ def radeon_logo
22
+ File.read(File.join(__dir__, '../../../assets/radeon_r_black_red_100x100.ascii'))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ ##
5
+ # A mixin to read fan details and validate input
6
+ module Fan
7
+
8
+ private
9
+
10
+ def fan_file(type)
11
+ @fan_file ||= {}
12
+ @fan_file[type] ||= "#{base_hwmon_dir}/fan1_#{type}"
13
+ end
14
+
15
+ def fan_mode_file
16
+ @fan_mode_file ||= "#{base_hwmon_dir}/pwm1_enable"
17
+ end
18
+
19
+ def fan_power_file
20
+ @fan_power_file ||= "#{base_hwmon_dir}/pwm1"
21
+ end
22
+
23
+ def fan_speed_raw
24
+ File.read(fan_power_file).strip
25
+ end
26
+
27
+ def fan_raw_speeds(type)
28
+ @fan_raw_speeds ||= {}
29
+ @fan_raw_speeds[type] ||= File.read(Dir.glob("#{base_card_dir}/**/pwm1_#{type}").first).to_i
30
+ end
31
+
32
+ ##
33
+ # Validate the raw fan speed is between the minimum and maximum values
34
+ # read from sysfs.
35
+ def valid_fan_raw_speed?(raw)
36
+ !raw.nil? && (fan_raw_speeds(:min)..fan_raw_speeds(:max)).cover?(raw.to_i)
37
+ end
38
+
39
+ def valid_fan_percent_speed?(percent)
40
+ (1..100.to_i).cover?(percent.to_i)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # A mixin to help with writing to system files
5
+ module SysWrite
6
+
7
+ private
8
+
9
+ ##
10
+ # Write to a system file with elevated priviledges.
11
+ def sudo_write(file_path, value)
12
+ `echo "#{value}" | sudo tee #{file_path}`
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/fan'
4
+ require_relative 'mixin/sys_write'
5
+
6
+ require_relative 'connector'
7
+
8
+ module AmdgpuFan
9
+ ## AmdgpuService
10
+ #
11
+ # A service class for reading and interacting with AMD radeon graphics cards
12
+ # through the amdgpu Linux kernel driver.
13
+ class Service
14
+ include Fan
15
+ include SysWrite
16
+
17
+ BASE_FOLDER = '/sys/class/drm'
18
+ FAN_MODES = { '1' => 'manual', '2' => 'auto' }.freeze
19
+
20
+ attr_reader :card_num
21
+
22
+ class Error < StandardError; end
23
+
24
+ def initialize(card_num: 0)
25
+ @card_num = card_num
26
+ end
27
+
28
+ def busy_percent
29
+ File.read("#{base_card_dir}/gpu_busy_percent").strip
30
+ end
31
+
32
+ def connectors
33
+ @connectors ||= Connector.where card_num: card_num
34
+ end
35
+
36
+ def core_clock
37
+ clock_from_pp_file "#{base_card_dir}/pp_dpm_sclk"
38
+ end
39
+
40
+ def fan_mode
41
+ FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
42
+ end
43
+
44
+ def fan_mode=(mode)
45
+ sudo_write fan_mode_file, FAN_MODES.key(mode.to_s)
46
+ end
47
+
48
+ def fan_speed=(value)
49
+ if valid_fan_percent_speed?(value)
50
+ new_raw = (value.to_f / 100 * fan_raw_speeds(:max).to_i).round
51
+ elsif valid_fan_raw_speed?(value)
52
+ new_raw = value
53
+ end
54
+
55
+ raise(self.class::Error, 'Invalid fan speed provided') if new_raw.to_s.empty?
56
+
57
+ self.fan_mode = :manual unless fan_mode == 'manual'
58
+
59
+ sudo_write fan_power_file, new_raw
60
+ end
61
+
62
+ def fan_speed_percent
63
+ (fan_speed_raw.to_f / fan_raw_speeds(:max).to_i * 100).round
64
+ end
65
+
66
+ def fan_speed_rpm
67
+ File.read(fan_file(:input)).strip
68
+ end
69
+
70
+ def memory_clock
71
+ clock_from_pp_file "#{base_card_dir}/pp_dpm_mclk"
72
+ end
73
+
74
+ def memory_total
75
+ File.read("#{base_card_dir}/mem_info_vram_total").to_i
76
+ end
77
+
78
+ def name
79
+ lspci_subsystem.split(': ')[1].strip
80
+ end
81
+
82
+ def power_dpm_state
83
+ File.read("#{base_card_dir}/power_dpm_state").strip
84
+ end
85
+
86
+ def power_draw
87
+ power_raw_to_watts File.read(power_avg_file)
88
+ end
89
+
90
+ def power_draw_percent
91
+ (power_draw.to_f / power_max.to_i * 100).round
92
+ end
93
+
94
+ def power_max
95
+ @power_max ||= power_raw_to_watts File.read("#{base_hwmon_dir}/power1_cap")
96
+ end
97
+
98
+ def profile_auto
99
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'auto'
100
+ end
101
+
102
+ def profile_force=(state)
103
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'manual'
104
+ sudo_write "#{base_card_dir}/pp_power_profile_mode", state
105
+ end
106
+
107
+ def profile_mode
108
+ File.read("#{base_card_dir}/pp_power_profile_mode").slice(/\w+\s*+\*/).delete('*').strip
109
+ end
110
+
111
+ def profile_summary
112
+ File.read("#{base_card_dir}/pp_power_profile_mode")
113
+ end
114
+
115
+ def temperature
116
+ (File.read(temperature_file).to_f / 1000).round(1)
117
+ end
118
+
119
+ def vbios_version
120
+ @vbios_version ||= File.read("#{base_card_dir}/vbios_version").strip
121
+ end
122
+
123
+ private
124
+
125
+ def base_card_dir
126
+ @base_card_dir ||= "#{BASE_FOLDER}/card#{card_num}/device"
127
+ end
128
+
129
+ def base_hwmon_dir
130
+ @base_hwmon_dir ||= Dir.glob("#{base_card_dir}/hwmon/hwmon*").first
131
+ end
132
+
133
+ def clock_from_pp_file(file)
134
+ File.read(file).slice(/\w+(?= \*)/)
135
+ end
136
+
137
+ def gpu_pci_id
138
+ @gpu_pci_id ||= `lspci -v | grep VGA`.split(' ').first
139
+ end
140
+
141
+ def lspci_subsystem
142
+ @lspci_subsystem ||= `lspci -v -s #{gpu_pci_id} | grep "Subsystem:"`
143
+ end
144
+
145
+ def power_avg_file
146
+ @power_avg_file ||= Dir.glob("#{base_card_dir}/**/power1_average").first
147
+ end
148
+
149
+ def power_raw_to_watts(raw_string)
150
+ (raw_string.strip.to_f / 1_000_000).round(2)
151
+ end
152
+
153
+ def temperature_file
154
+ @temperature_file ||= Dir.glob("#{base_card_dir}/**/temp1_input").first
155
+ end
156
+ end
157
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amdgpu_fan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin McCormack
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-17 00:00:00.000000000 Z
11
+ date: 2020-03-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A CLI for interacting with the amdgpu Linux driver
14
14
  email: harlemsquirrel@gmail.com
@@ -20,9 +20,13 @@ files:
20
20
  - README.md
21
21
  - bin/amdgpu_fan
22
22
  - config/environment.rb
23
- - lib/amdgpu_fan_cli.rb
24
- - lib/amdgpu_service.rb
25
- - lib/radeon_r_black_red_100x100.ascii
23
+ - lib/amdgpu_fan.rb
24
+ - lib/amdgpu_fan/cli.rb
25
+ - lib/amdgpu_fan/connector.rb
26
+ - lib/amdgpu_fan/mixin/cli_output_format.rb
27
+ - lib/amdgpu_fan/mixin/fan.rb
28
+ - lib/amdgpu_fan/mixin/sys_write.rb
29
+ - lib/amdgpu_fan/service.rb
26
30
  homepage: https://github.com/HarlemSquirrel/amdgpu-fan-rb
27
31
  licenses:
28
32
  - MIT
@@ -42,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
42
46
  - !ruby/object:Gem::Version
43
47
  version: '0'
44
48
  requirements: []
45
- rubygems_version: 3.0.6
49
+ rubygems_version: 3.1.2
46
50
  signing_key:
47
51
  specification_version: 4
48
52
  summary: A CLI to view and set fan speeds for AMD graphics cards running on the open
@@ -1,167 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../config/environment'
4
-
5
- # The main class
6
- class AmdgpuFanCli < Thor
7
- METER_CHAR = '*'
8
- WATCH_FIELD_SEPARATOR = ' | '
9
-
10
- desc 'auto', 'Set fan mode to automatic (requires sudo)'
11
- def auto
12
- amdgpu_service.fan_mode = :auto
13
- puts fan_status
14
- end
15
-
16
- desc 'connectors', 'View the status of the display connectors'
17
- def connectors
18
- amdgpu_service.connectors_status.each do |connector, status|
19
- puts "#{connector}: #{status}"
20
- end
21
- end
22
-
23
- desc 'profile', 'View power profile details.'
24
- def profile
25
- puts amdgpu_service.profile_summary
26
- end
27
-
28
- desc 'profile_auto', 'Set the power profile to automatic mode.'
29
- def profile_auto
30
- amdgpu_service.profile_auto
31
- puts amdgpu_service.profile_summary
32
- end
33
-
34
- desc 'profile_force [PROFILE_NUM]', 'Manually set a power profile.'
35
- def profile_force(state)
36
- amdgpu_service.profile_force = state
37
- puts amdgpu_service.profile_summary
38
- end
39
-
40
- desc 'set PERCENTAGE', 'Set fan speed to PERCENTAGE (requires sudo)'
41
- def set(percentage)
42
- return puts 'Invalid percentage' unless (0..100).cover?(percentage.to_i)
43
-
44
- amdgpu_service.set_fan_manual_speed! percent: percentage
45
- puts fan_status
46
- rescue AmdgpuService::Error
47
- puts 'Invalid fan speed provided. The percentage should be between 1 and 100'
48
- exit 1
49
- end
50
-
51
- desc 'status [--logo]', 'View device info, current fan speed, and temperature'
52
- def status(option = nil)
53
- puts radeon_logo if option == '--logo'
54
- puts "📺 #{'GPU:'.ljust(7)} #{amdgpu_service.name}",
55
- "📄 #{'vBIOS:'.ljust(7)} #{amdgpu_service.vbios_version}",
56
- "⏰ #{'Clocks:'.ljust(7)} #{clock_status}",
57
- "🌀 #{'Fan:'.ljust(7)} #{fan_status}",
58
- "🌞 #{'Temp:'.ljust(7)} #{amdgpu_service.temperature}°C",
59
- "⚡ #{'Power:'.ljust(7)} #{amdgpu_service.profile_mode} profile in " \
60
- "#{amdgpu_service.power_dpm_state} mode using " \
61
- "#{amdgpu_service.power_draw} / #{amdgpu_service.power_max} Watts "\
62
- "(#{amdgpu_service.power_draw_percent}%)",
63
- "⚖ #{'Load:'.ljust(7)} #{percent_meter amdgpu_service.busy_percent, 20}"
64
- end
65
-
66
- desc 'watch [SECONDS]', 'Watch fan speed, load, power, and temperature ' \
67
- 'refreshed every n seconds'
68
- def watch(seconds = 1)
69
- return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
70
-
71
- puts "Watching #{amdgpu_service.name} every #{seconds} second(s)...",
72
- ' <Press Ctrl-C to exit>'
73
-
74
- trap 'SIGINT' do
75
- puts "\nAnd now the watch is ended."
76
- exit 0
77
- end
78
-
79
- loop do
80
- time = Time.now
81
- puts [time.strftime('%F %T'), summary_clock, summary_fan, summary_load, summary_power,
82
- summary_temp].join(WATCH_FIELD_SEPARATOR)
83
-
84
- # It can take a second or two to run the above so we remove them from the wait
85
- # here to get a more consistant watch interval.
86
- sec_left_to_wait = time.to_i + seconds.to_i - Time.now.to_i
87
- sleep sec_left_to_wait if sec_left_to_wait.positive?
88
- end
89
- end
90
-
91
- desc 'watch_csv [SECONDS]', 'Watch stats in CSV format ' \
92
- 'refreshed every n seconds defaulting to 1 second'
93
- def watch_csv(seconds = 1)
94
- return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
95
-
96
- puts 'Timestamp, Core Clock (Mhz),Memory Clock (Mhz),Fan speed (rpm), '\
97
- 'Load (%),Power (Watts),Temp (°C)'
98
-
99
- trap 'SIGINT' do
100
- exit 0
101
- end
102
-
103
- loop do
104
- puts [Time.now.strftime('%F %T'),
105
- amdgpu_service.core_clock,
106
- amdgpu_service.memory_clock,
107
- amdgpu_service.fan_speed_rpm,
108
- amdgpu_service.busy_percent,
109
- amdgpu_service.power_draw,
110
- amdgpu_service.temperature].join(',')
111
- sleep seconds.to_i
112
- end
113
- end
114
-
115
- private
116
-
117
- def amdgpu_service
118
- @amdgpu_service ||= AmdgpuService.new
119
- end
120
-
121
- def clock_status
122
- "#{amdgpu_service.core_clock} Core, #{amdgpu_service.memory_clock} Memory"
123
- end
124
-
125
- def current_time
126
- Time.now.strftime('%F %T')
127
- end
128
-
129
- def fan_status
130
- "#{amdgpu_service.fan_mode} mode running at " \
131
- "#{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%)"
132
- end
133
-
134
- def percent_meter(percent, length = 10)
135
- progress_bar_count = (length * percent.to_f / 100).round
136
- percent_string = "#{percent}%".ljust(4)
137
- "[#{METER_CHAR * progress_bar_count}#{' ' * (length - progress_bar_count)}]#{percent_string}"
138
- end
139
-
140
- def radeon_logo
141
- File.read(File.join(__dir__, '../lib/radeon_r_black_red_100x100.ascii'))
142
- end
143
-
144
- def summary_clock
145
- "Core: #{amdgpu_service.core_clock.ljust(7)} #{WATCH_FIELD_SEPARATOR}"\
146
- "Memory: #{amdgpu_service.memory_clock.ljust(7)}"
147
- end
148
-
149
- def summary_fan
150
- fan_speed_string = "#{amdgpu_service.fan_speed_rpm} rpm".ljust(8)
151
- "Fan: #{fan_speed_string} #{percent_meter(amdgpu_service.fan_speed_percent)}"
152
- end
153
-
154
- def summary_load
155
- "Load: #{percent_meter amdgpu_service.busy_percent}"
156
- end
157
-
158
- def summary_power
159
- power_string = "#{amdgpu_service.power_draw} W".ljust(amdgpu_service.power_max.to_s.length + 3)
160
- "Power: #{power_string} #{percent_meter amdgpu_service.power_draw_percent}"
161
- end
162
-
163
- def summary_temp
164
- temp_string = "#{amdgpu_service.temperature}°C".ljust(7)
165
- "Temp: #{temp_string}"
166
- end
167
- end
@@ -1,177 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ## AmdgpuService
4
- #
5
- # A service class for reading and interacting with AMD radeon graphics cards
6
- # through the amdgpu Linux kernel driver.
7
- class AmdgpuService
8
- BASE_FOLDER = '/sys/class/drm'
9
- FAN_MODES = { '1' => 'manual', '2' => 'auto' }.freeze
10
-
11
- attr_reader :card_num
12
-
13
- class Error < StandardError; end
14
-
15
- def initialize(card_num: 0)
16
- @card_num = card_num
17
- end
18
-
19
- def busy_percent
20
- File.read("#{base_card_folder}/gpu_busy_percent").strip
21
- end
22
-
23
- def connectors_status
24
- connectors_files.each_with_object({}) do |f, connectors|
25
- connectors[f.slice(/(?<=card0-)(\w|-)+/)] = File.read(f).strip
26
- end
27
- end
28
-
29
- def core_clock
30
- clock_from_pp_file "#{base_card_folder}/pp_dpm_sclk"
31
- end
32
-
33
- def fan_mode
34
- FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
35
- end
36
-
37
- def fan_mode=(mode)
38
- `echo "#{FAN_MODES.key(mode.to_s)}" | sudo tee #{fan_mode_file}`
39
- end
40
-
41
- def fan_speed_percent
42
- (fan_speed_raw.to_f / fan_speed_raw_max.to_i * 100).round
43
- end
44
-
45
- def fan_speed_raw_max
46
- @fan_speed_raw_max ||= File.read(Dir.glob("#{base_card_folder}/**/pwm1_max").first).strip
47
- end
48
-
49
- def fan_speed_rpm
50
- File.read(fan_input_file).strip
51
- end
52
-
53
- def memory_clock
54
- clock_from_pp_file "#{base_card_folder}/pp_dpm_mclk"
55
- end
56
-
57
- def name
58
- lspci_subsystem.split(': ')[1].strip
59
- end
60
-
61
- def power_dpm_state
62
- File.read("#{base_card_folder}/power_dpm_state").strip
63
- end
64
-
65
- def power_draw
66
- power_raw_to_watts File.read(power_avg_file)
67
- end
68
-
69
- def power_draw_percent
70
- (power_draw.to_f / power_max.to_i * 100).round
71
- end
72
-
73
- def power_max
74
- @power_max ||= power_raw_to_watts File.read(power_max_file)
75
- end
76
-
77
- def profile_auto
78
- `echo "auto" | sudo tee "#{base_card_folder}/power_dpm_force_performance_level"`
79
- end
80
-
81
- def profile_force=(state)
82
- `echo "manual" | sudo tee "#{base_card_folder}/power_dpm_force_performance_level"`
83
- `echo "#{state}" | sudo tee "#{base_card_folder}/pp_power_profile_mode"`
84
- end
85
-
86
- def profile_mode
87
- File.read("#{base_card_folder}/pp_power_profile_mode").slice(/\w+ \*/).delete('*').strip
88
- end
89
-
90
- def profile_summary
91
- File.read("#{base_card_folder}/pp_power_profile_mode")
92
- end
93
-
94
- def set_fan_manual_speed!(percent: nil, raw: nil)
95
- if valid_fan_percent_speed?(percent)
96
- new_raw = (percent.to_f / 100 * fan_speed_raw_max.to_i).round
97
- elsif valid_fan_raw_speed?(raw)
98
- new_raw = raw
99
- end
100
-
101
- raise(self.class::Error, 'Invalid fan speed provided') if new_raw.to_s.empty?
102
-
103
- fan_mode = :manual unless fan_mode == 'manual'
104
-
105
- `echo "#{new_raw}" | sudo tee #{fan_power_file}`
106
- end
107
-
108
- def temperature
109
- (File.read(temperature_file).to_f / 1000).round(1)
110
- end
111
-
112
- def vbios_version
113
- @vbios_version ||= File.read("#{base_card_folder}/vbios_version").strip
114
- end
115
-
116
- private
117
-
118
- def base_card_folder
119
- @base_card_folder ||= "#{BASE_FOLDER}/card#{card_num}/device"
120
- end
121
-
122
- def clock_from_pp_file(file)
123
- File.read(file).slice(/\w+(?= \*)/)
124
- end
125
-
126
- def connectors_files
127
- @connectors_files ||= Dir["/sys/class/drm/card#{card_num}*/status"].sort
128
- end
129
-
130
- def fan_input_file
131
- @fan_input_file ||= Dir.glob("#{base_card_folder}/**/fan1_input").first
132
- end
133
-
134
- def fan_mode_file
135
- @fan_mode_file ||= Dir.glob("#{base_card_folder}/**/pwm1_enable").first
136
- end
137
-
138
- def fan_power_file
139
- @fan_power_file ||= Dir.glob("#{base_card_folder}/**/pwm1").first
140
- end
141
-
142
- def fan_speed_raw
143
- File.read(fan_power_file).strip
144
- end
145
-
146
- def gpu_pci_id
147
- @gpu_pci_id ||= `lspci -v | grep VGA`.split(' ').first
148
- end
149
-
150
- def lspci_subsystem
151
- @lspci_subsystem ||= `lspci -v -s #{gpu_pci_id} | grep "Subsystem:"`
152
- end
153
-
154
- def power_avg_file
155
- @power_avg_file ||= Dir.glob("#{base_card_folder}/**/power1_average").first
156
- end
157
-
158
- def power_max_file
159
- @power_max_file ||= Dir.glob("#{base_card_folder}/**/power1_cap").first
160
- end
161
-
162
- def power_raw_to_watts(raw_string)
163
- (raw_string.strip.to_f / 1_000_000).round(2)
164
- end
165
-
166
- def temperature_file
167
- @temperature_file ||= Dir.glob("#{base_card_folder}/**/temp1_input").first
168
- end
169
-
170
- def valid_fan_raw_speed?(raw)
171
- (1..fan_speed_raw_max.to_i).cover?(raw.to_i)
172
- end
173
-
174
- def valid_fan_percent_speed?(percent)
175
- (1..100.to_i).cover?(percent.to_i)
176
- end
177
- end
@@ -1,40 +0,0 @@
1
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
3
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
4
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
5
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
6
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
7
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
8
- ''''''''''''''.                                        .''''''''''''''''''''''''
9
- ''''''''''''''.                                          .''''''''''''''''''''''
10
- ''''''''''''''.                                    ..'''''''''''''''''''
11
- ''''''''''''''.                                          .''''''''''''''''''
12
- ''''''''''''''.                             ..                ..''''''''''''''''
13
- ''''''''''''''.         .'''''''''''''''''''''''..           .''''''''''''''''
14
- ''''''''''''''.           ''''''''''''''''''''''''''.          .''''''''''''''''
15
- ''''''''''''''.           ''''''''''''''''''''''''''.         .''''''''''''''''
16
- ''''''''''''''.           ''''''''''''''''''''''''''.          .''''''''''''''''
17
- ''''''''''''''.           ''''''''''''''''''''''''''.        .''''''''''''''''
18
- ''''''''''''''.           ''''''''''''''''''''''''''.          .''''''''''''''''
19
- ''''''''''''''.           ''''''''''''''''''''''''''.          .''''''''''''''''
20
- ''''''''''''''.           '''''''''''''''''''''''..          .''''''''''''''''
21
- ''''''''''''''.                                               .''''''''''''''''
22
- ''''''''''''''.                                            .''''''''''''''''''
23
- ''''''''''''''.                                            ..'''''''''''''''''''
24
- ''''''''''''''.                                          .''''''''''''''''''''''
25
- ''''''''''''''.                                         .'''''''''''''''''''''''
26
- ''''''''''''''.           ''''''''''''''''..           .'''''''''''''''''''''''
27
- ''''''''''''''.           ''''''''''''''''''.           .''''''''''''''''''''''
28
- ''''''''''''''.           '''''''''''''''''''.            .''''''''''''''''''''
29
- ''''''''''''''.           ''''''''''''''''''''..            ..''''''''''''''''''
30
- ''''''''''''''.           ''''''''''''''''''''''.            .'''''''''''''''''
31
- ''''''''''''''.           '''''''''''''''''''''''..          ..'''''''''''''''
32
- ''''''''''''''.           '''''''''''''''''''''''''.             .''''''''''''''
33
- ''''''''''''''.           ''''''''''''''''''''''''''.             .'''''''''''''
34
- ''''''''''''''............''''''''''''''''''''''''''''..............''''''''''''
35
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
36
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
37
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
38
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
39
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
40
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''