amdgpu_fan 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''