amdgpu_fan 0.3.0 → 0.7.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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ ##
5
+ # A mixin to read fan details and validate input
6
+ module Fan
7
+ private
8
+
9
+ def fan_file(type)
10
+ @fan_file ||= {}
11
+ @fan_file[type] ||= "#{base_hwmon_dir}/fan1_#{type}"
12
+ end
13
+
14
+ def fan_mode_file
15
+ @fan_mode_file ||= "#{base_hwmon_dir}/pwm1_enable"
16
+ end
17
+
18
+ def fan_power_file
19
+ @fan_power_file ||= "#{base_hwmon_dir}/pwm1"
20
+ end
21
+
22
+ def fan_speed_raw
23
+ File.read(fan_power_file).strip
24
+ end
25
+
26
+ def fan_raw_speeds(type)
27
+ @fan_raw_speeds ||= {}
28
+ @fan_raw_speeds[type] ||= File.read(Dir.glob("#{base_card_dir}/**/pwm1_#{type}").first).to_i
29
+ end
30
+
31
+ ##
32
+ # Validate the raw fan speed is between the minimum and maximum values
33
+ # read from sysfs.
34
+ def valid_fan_raw_speed?(raw)
35
+ !raw.nil? && (fan_raw_speeds(:min)..fan_raw_speeds(:max)).cover?(raw.to_i)
36
+ end
37
+
38
+ def valid_fan_percent_speed?(percent)
39
+ (1..100.to_i).cover?(percent.to_i)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # A mixin to help with writing to system files
5
+ module SysWrite
6
+ private
7
+
8
+ ##
9
+ # Write to a system file with elevated priviledges.
10
+ def sudo_write(file_path, value)
11
+ `echo "#{value}" | sudo tee #{file_path}`
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module AmdgpuFan
6
+ ## PCI
7
+ #
8
+ # Retrieve device information from PCI IDs.
9
+ # https://pci-ids.ucw.cz/
10
+ #
11
+ class PCI
12
+ TMP_FILE = '/tmp/pci.ids.txt'
13
+ URL = 'https://pci-ids.ucw.cz/v2.2/pci.ids'
14
+
15
+ ##
16
+ # Return the device name if available
17
+ #
18
+ def self.device_name(vendor_id, device_id, subdevice_id)
19
+ device = info.dig vendor_id, :devices, device_id
20
+
21
+ device.dig(subdevice_id, :name) || device[:name]
22
+ end
23
+
24
+ def self.vendor_name(vendor_id)
25
+ info.dig vendor_id, :name
26
+ end
27
+
28
+ def self.info
29
+ current_vendor_id = nil
30
+ current_device_id = nil
31
+
32
+ @info = raw.each_line.with_object({}) do |line, hash|
33
+ next if line.empty? || line.start_with?('#')
34
+
35
+ if line[0] =~ /(\d|[a-z])/
36
+ # Vendor line
37
+ current_vendor_id = line.split(' ').first.to_i(16)
38
+ vendor_name = line.split(' ').last.strip
39
+ hash[current_vendor_id] = { name: vendor_name, devices: {} }
40
+ elsif line[0..1] =~ /\t\w/
41
+ # Device line
42
+ current_device_id = line.split(' ').first.to_i(16)
43
+ device_name = line.split(' ').last.strip
44
+ hash[current_vendor_id][:devices][current_device_id] = { name: device_name }
45
+ elsif line[0..2] =~ /\t\t\w/
46
+ # Subvendor line
47
+ subvendor_id = line.split(' ').first.to_i(16)
48
+ subdevice_id = line.split(' ')[1].to_i(16)
49
+ subdevice_name = line.split(' ')[1].strip
50
+ hash[current_vendor_id][:devices][current_device_id][subdevice_id] =
51
+ { name: subdevice_name }
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.raw
57
+ File.write(TMP_FILE, Net::HTTP.get(URI(URL))) unless File.exist?(TMP_FILE)
58
+
59
+ @raw ||= File.read(TMP_FILE)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/fan'
4
+ require_relative 'mixin/sys_write'
5
+
6
+ require_relative 'connector'
7
+ require_relative 'pci'
8
+
9
+ module AmdgpuFan
10
+ ## AmdgpuService
11
+ #
12
+ # A service class for reading and interacting with AMD radeon graphics cards
13
+ # through the amdgpu Linux kernel driver.
14
+ class Service
15
+ include Fan
16
+ include SysWrite
17
+
18
+ BASE_FOLDER = '/sys/class/drm'
19
+ FAN_MODES = { '1' => 'manual', '2' => 'auto' }.freeze
20
+
21
+ attr_reader :card_num
22
+
23
+ class Error < StandardError; end
24
+
25
+ def initialize(card_num: 0)
26
+ @card_num = card_num
27
+ end
28
+
29
+ def busy_percent
30
+ File.read("#{base_card_dir}/gpu_busy_percent").strip
31
+ end
32
+
33
+ def connectors
34
+ @connectors ||= Connector.where card_num: card_num
35
+ end
36
+
37
+ def core_clock
38
+ clock_from_pp_file "#{base_card_dir}/pp_dpm_sclk"
39
+ end
40
+
41
+ def display_names
42
+ connectors.map(&:display_name).compact
43
+ end
44
+
45
+ def fan_mode
46
+ FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
47
+ end
48
+
49
+ ##
50
+ # Set the fan mode to auto or manual.
51
+ #
52
+ def fan_mode=(mode)
53
+ sudo_write fan_mode_file, FAN_MODES.key(mode.to_s)
54
+ end
55
+
56
+ ##
57
+ # Set the fan speed to a percentage if <= 100 or a raw value
58
+ #
59
+ def fan_speed=(value)
60
+ if valid_fan_percent_speed?(value)
61
+ new_raw = (value.to_f / 100 * fan_raw_speeds(:max).to_i).round
62
+ elsif valid_fan_raw_speed?(value)
63
+ new_raw = value
64
+ end
65
+
66
+ raise(self.class::Error, 'Invalid fan speed provided') if new_raw.to_s.empty?
67
+
68
+ self.fan_mode = :manual unless fan_mode == 'manual'
69
+
70
+ sudo_write fan_power_file, new_raw
71
+ end
72
+
73
+ def fan_speed_percent
74
+ (fan_speed_raw.to_f / fan_raw_speeds(:max).to_i * 100).round
75
+ end
76
+
77
+ ##
78
+ # Return the fan speed in revolutions per minute
79
+ #
80
+ def fan_speed_rpm
81
+ File.read(fan_file(:input)).strip
82
+ end
83
+
84
+ ##
85
+ # Return the current memory clock speed.
86
+ #
87
+ def memory_clock
88
+ clock_from_pp_file "#{base_card_dir}/pp_dpm_mclk"
89
+ end
90
+
91
+ ##
92
+ # Return the total memory on the card in bytes
93
+ #
94
+ def memory_total
95
+ File.read("#{base_card_dir}/mem_info_vram_total").to_i
96
+ end
97
+
98
+ def model_id
99
+ @model_id ||= File.read(File.join(base_card_dir, '/subsystem_device')).gsub(/\A0x/, '').upcase
100
+ end
101
+
102
+ def name
103
+ return 'Unknown' unless PCI.vendor_name(vendor_id)
104
+
105
+ [PCI.vendor_name(vendor_id),
106
+ PCI.device_name(vendor_id, device_id, subdevice_id) || 'unknown']
107
+ .join(' ')
108
+ end
109
+
110
+ def power_dpm_state
111
+ File.read("#{base_card_dir}/power_dpm_state").strip
112
+ end
113
+
114
+ def power_draw
115
+ power_raw_to_watts File.read(power_avg_file)
116
+ end
117
+
118
+ def power_draw_percent
119
+ (power_draw.to_f / power_max.to_i * 100).round
120
+ end
121
+
122
+ def power_max
123
+ @power_max ||= power_raw_to_watts File.read("#{base_hwmon_dir}/power1_cap")
124
+ end
125
+
126
+ def profile_auto
127
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'auto'
128
+ end
129
+
130
+ def profile_force=(state)
131
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'manual'
132
+ sudo_write "#{base_card_dir}/pp_power_profile_mode", state
133
+ end
134
+
135
+ def profile_mode
136
+ File.read("#{base_card_dir}/pp_power_profile_mode").slice(/\w+\s*+\*/).delete('*').strip
137
+ end
138
+
139
+ def profile_summary
140
+ File.read("#{base_card_dir}/pp_power_profile_mode")
141
+ end
142
+
143
+ def temperature
144
+ (File.read(temperature_file).to_f / 1000).round(1)
145
+ end
146
+
147
+ def vbios_version
148
+ @vbios_version ||= File.read("#{base_card_dir}/vbios_version").strip
149
+ end
150
+
151
+ private
152
+
153
+ def base_card_dir
154
+ @base_card_dir ||= "#{BASE_FOLDER}/card#{card_num}/device"
155
+ end
156
+
157
+ def base_hwmon_dir
158
+ @base_hwmon_dir ||= Dir.glob("#{base_card_dir}/hwmon/hwmon*").first
159
+ end
160
+
161
+ def clock_from_pp_file(file)
162
+ File.read(file).slice(/\w+(?= \*)/)
163
+ end
164
+
165
+ def device_id
166
+ @device_id ||= File.read(File.join(base_card_dir, 'device')).to_i(16)
167
+ end
168
+
169
+ def power_avg_file
170
+ @power_avg_file ||= Dir.glob("#{base_card_dir}/**/power1_average").first
171
+ end
172
+
173
+ def power_raw_to_watts(raw_string)
174
+ (raw_string.strip.to_f / 1_000_000).round(2)
175
+ end
176
+
177
+ def subdevice_id
178
+ @subdevice_id ||= File.read(File.join(base_card_dir, 'subsystem_device')).to_i(16)
179
+ end
180
+
181
+ def temperature_file
182
+ @temperature_file ||= Dir.glob("#{base_card_dir}/**/temp1_input").first
183
+ end
184
+
185
+ def vendor_id
186
+ @vendor_id ||= File.read(File.join(base_card_dir, 'vendor')).to_i(16)
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # A set of stats
5
+ class StatSet < Hash
6
+ attr_accessor :avg, :max, :min, :now
7
+ attr_reader :unit
8
+
9
+ def initialize(unit)
10
+ @unit = unit
11
+ end
12
+
13
+ def stats
14
+ { min: min, avg: avg, max: max, now: now }
15
+ end
16
+
17
+ ##
18
+ # Return a string containing all the stats with units.
19
+ #
20
+ def to_s
21
+ stats.map { |k, v| "#{k}: #{v.to_s.rjust(6)} #{unit.ljust(3)} " }.join
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmdgpuFan
4
+ # Current version of RSpec Core, in semantic versioning format.
5
+ VERSION = '0.7.0'
6
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'stat_set'
4
+
5
+ module AmdgpuFan
6
+ # Keep track of stats over time.
7
+ class Watcher
8
+ attr_reader :core_clock, :fan_speed, :num_measurements, :mem_clock, :power, :temp
9
+
10
+ def initialize(amdgpu_service)
11
+ @amdgpu_service = amdgpu_service
12
+ @num_measurements = 0
13
+
14
+ @core_clock = StatSet.new 'MHz'
15
+ @mem_clock = StatSet.new 'MHz'
16
+ @fan_speed = StatSet.new 'RPM'
17
+ @power = StatSet.new 'W'
18
+ @temp = StatSet.new '°C'
19
+ end
20
+
21
+ ##
22
+ # Take a new set of measurements and adjust the stats.
23
+ #
24
+ def measure
25
+ @num_measurements += 1
26
+
27
+ @core_clock.now = @amdgpu_service.core_clock.to_i
28
+ @mem_clock.now = @amdgpu_service.memory_clock.to_i
29
+ @fan_speed.now = @amdgpu_service.fan_speed_rpm.to_i
30
+ @power.now = @amdgpu_service.power_draw.to_f
31
+ @temp.now = @amdgpu_service.temperature.to_i
32
+
33
+ [@core_clock, @mem_clock, @fan_speed, @power, @temp].each do |stat_set|
34
+ calculate_stats(stat_set)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def calculate_stats(stat_set)
41
+ if num_measurements == 1
42
+ stat_set.min = stat_set.now
43
+ stat_set.avg = stat_set.now.to_f
44
+ stat_set.max = stat_set.now
45
+ return
46
+ end
47
+
48
+ stat_set.min = stat_set.now if stat_set.now < stat_set.min
49
+ stat_set.avg =
50
+ ((stat_set.now + stat_set.avg * (num_measurements - 1)) / num_measurements.to_f)
51
+ .round(1)
52
+ stat_set.max = stat_set.now if stat_set.now > stat_set.max
53
+ end
54
+ end
55
+ end
metadata CHANGED
@@ -1,16 +1,58 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amdgpu_fan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin McCormack
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-14 00:00:00.000000000 Z
12
- dependencies: []
13
- description: A CLI for amdgpu fans
11
+ date: 2020-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A CLI for interacting with the amdgpu Linux driver
14
56
  email: harlemsquirrel@gmail.com
15
57
  executables:
16
58
  - amdgpu_fan
@@ -20,14 +62,23 @@ files:
20
62
  - README.md
21
63
  - bin/amdgpu_fan
22
64
  - config/environment.rb
23
- - lib/amdgpu_fan_cli.rb
24
- - lib/amdgpu_service.rb
25
- - lib/radeon_r_black_red_100x100.ascii
65
+ - config/icons.yml
66
+ - lib/amdgpu_fan.rb
67
+ - lib/amdgpu_fan/cli.rb
68
+ - lib/amdgpu_fan/connector.rb
69
+ - lib/amdgpu_fan/mixin/cli_output_format.rb
70
+ - lib/amdgpu_fan/mixin/fan.rb
71
+ - lib/amdgpu_fan/mixin/sys_write.rb
72
+ - lib/amdgpu_fan/pci.rb
73
+ - lib/amdgpu_fan/service.rb
74
+ - lib/amdgpu_fan/stat_set.rb
75
+ - lib/amdgpu_fan/version.rb
76
+ - lib/amdgpu_fan/watcher.rb
26
77
  homepage: https://github.com/HarlemSquirrel/amdgpu-fan-rb
27
78
  licenses:
28
79
  - MIT
29
80
  metadata: {}
30
- post_install_message:
81
+ post_install_message:
31
82
  rdoc_options: []
32
83
  require_paths:
33
84
  - lib
@@ -42,8 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
42
93
  - !ruby/object:Gem::Version
43
94
  version: '0'
44
95
  requirements: []
45
- rubygems_version: 3.0.3
46
- signing_key:
96
+ rubygems_version: 3.1.2
97
+ signing_key:
47
98
  specification_version: 4
48
99
  summary: A CLI to view and set fan speeds for AMD graphics cards running on the open
49
100
  source amdgpu Linux driver