amdgpu_fan 0.5.0 → 0.8.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: 822ba3e84cdd6cb64680ace91e696351c3ac8f61fe7344554304be517c1ee141
4
- data.tar.gz: b74858f3916aec38afb02134694011f59f4c672d276dd4905e9da6accee69333
3
+ metadata.gz: 1fb09354467c6ffeb603c4cbc72b131c626e83d4403148ebeb5fac342222ac29
4
+ data.tar.gz: b62420cbf660f704682f34565128c749216bb5605b3d57b843f4bc85671aa09c
5
5
  SHA512:
6
- metadata.gz: 1798fa2cb6a578717c75d898501833e75e27fc8efd14881954c8a7cdf53e71a0dc1dfb0cba6b51b8b96572943761d6d9f23be33f26f8bc6ee3c42e38f017ab7c
7
- data.tar.gz: 95df0c6e478a1cbe580695c9be8cf123e555ec76ef767eb5a91b24d174693667cf33b0dabcc6a3ddd45cdfd54a7efd070317e6a74fc63395f2b96d207b77e0eb
6
+ metadata.gz: f9a94891e8024c9e2c58c46feb4b7654e69978a9215278d9588065db177e8df447670cad9be726f608f16bd77f4a26e2a05fe16c536ce42bce0c9242489d2b43
7
+ data.tar.gz: 8046858789272ed17ce75304f53ee3915973193bc81bd301335448456c99bd8d4d774962668b2b98388a24301e158d4cffb18a65a28d27c7fd45c130c639740d
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # amdgpu_fan
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/amdgpu_fan.svg)](https://badge.fury.io/rb/amdgpu_fan)
3
4
  [![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
5
 
5
6
  A Ruby CLI to read and set fan speed, power profiles, and more for AMD Radeon graphics cards running on the AMDGPU Linux driver.
@@ -74,8 +75,30 @@ Watching Advanced Micro Devices, Inc. [AMD/ATI] Radeon R9 FURY X / NANO every 3
74
75
  And now the watch is ended.
75
76
  ```
76
77
 
78
+ ```
79
+ ➤ bin/amdgpu_fan watch_avg
80
+ Watching Sapphire Technology Limited Vega 10 XL/XT [Radeon RX Vega 56/64] min, max and averges since
81
+ 2020-06-02 23:05:20 -0400...
82
+ <Press Ctrl-C to exit>
83
+ ⏰ Core clock min: 852 MHz avg: 887.0 MHz max: 1200 MHz now: 852 MHz
84
+ 💾 Memory clk min: 167 MHz avg: 227.1 MHz max: 945 MHz now: 167 MHz
85
+ 🌀 Fan speed min: 1231 RPM avg: 1231.0 RPM max: 1231 RPM now: 1231 RPM
86
+ ⚡ Power usage min: 6.0 W avg: 21.8 W max: 141.0 W now: 6.0 W
87
+ 🌡 Temperature min: 30 °C avg: 31.3 °C max: 35 °C now: 32 °C
88
+ ^C
89
+ And now the watch is ended.
90
+ ```
91
+
77
92
  ## Dependencies
78
93
 
79
94
  - [Ruby](https://www.ruby-lang.org) with [Bundler](https://bundler.io)
80
95
  - [Thor](http://whatisthor.com/) (installed with `bundle install`)
81
- - [`lspci`](https://linux.die.net/man/8/lspci) (included with most Linux distributions)
96
+ - Internet connection - Device info is retrieved from https://pci-ids.ucw.cz/
97
+
98
+ ## Building a binary
99
+
100
+ [Ruby Packer](https://github.com/pmq20/ruby-packer) provides a convenient way to compile this into a single executable. For the best results, compile Ruby Packer from source from the lastest master branch.
101
+
102
+ ```sh
103
+ rubyc amdgpu_fan --output amdgpu_fan
104
+ ```
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'async'
3
4
  require 'thor'
4
5
 
5
6
  require_relative '../lib/amdgpu_fan'
7
+ require_relative '../lib/amdgpu_fan/version'
6
8
 
7
9
  require_relative '../lib/amdgpu_fan/mixin/cli_output_format'
8
10
  require_relative '../lib/amdgpu_fan/mixin/sys_write'
9
11
 
10
12
  require_relative '../lib/amdgpu_fan/service'
11
13
  require_relative '../lib/amdgpu_fan/cli'
14
+ require_relative '../lib/amdgpu_fan/pci'
15
+ require_relative '../lib/amdgpu_fan/stat_set'
16
+ require_relative '../lib/amdgpu_fan/watcher'
data/config/icons.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ clock: ⏰
3
+ display: 📺
4
+ fan: 🌀
5
+ gpu: 👾
6
+ load: "⚖ "
7
+ memory: 💾
8
+ power: ⚡
9
+ temp: "🌡 "
10
+ vbios: 📄
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength
4
+
5
+ require 'yaml'
6
+
3
7
  module AmdgpuFan
4
8
  # The command-line interface class
5
9
  class Cli < Thor
6
10
  include CliOutputFormat
7
11
 
12
+ ICONS = YAML.safe_load(File.read(File.join(__dir__, '../../config/icons.yml')))
13
+ .transform_keys(&:to_sym).freeze
8
14
  WATCH_FIELD_SEPARATOR = ' | '
9
15
 
10
16
  desc 'connectors', 'View the status of the display connectors.'
@@ -52,22 +58,28 @@ module AmdgpuFan
52
58
  desc 'status [--logo]', 'View device info, current fan speed, and temperature.'
53
59
  def status(option = nil)
54
60
  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}"
61
+ puts ICONS[:gpu] + ' GPU:'.ljust(9) + amdgpu_service.name
62
+ puts ICONS[:vbios] + ' vBIOS:'.ljust(9) + amdgpu_service.vbios_version
63
+ puts "#{ICONS[:display]} Displays:#{amdgpu_service.display_names.join(', ')}"
64
+ puts ICONS[:clock] + ' Clocks:'.ljust(9) + clock_status
65
+ puts ICONS[:memory] + ' Memory:'.ljust(9) + mem_total_mibibyes
66
+ puts ICONS[:fan] + ' Fan:'.ljust(9) + fan_status
67
+ puts ICONS[:temp] + ' Temp:'.ljust(9) + "#{amdgpu_service.temperature}°C"
68
+ puts ICONS[:power] + ' Power:'.ljust(9) +
69
+ "#{amdgpu_service.profile_mode} profile in " \
70
+ "#{amdgpu_service.power_dpm_state} mode using " \
71
+ "#{amdgpu_service.power_draw} / #{amdgpu_service.power_max} Watts "\
72
+ "(#{amdgpu_service.power_draw_percent}%)"
73
+ puts ICONS[:load] + ' Load:'.ljust(9) + percent_meter(amdgpu_service.busy_percent)
74
+ end
75
+
76
+ desc 'version', 'Print the application version.'
77
+ def version
78
+ puts AmdgpuFan::VERSION
67
79
  end
68
80
 
69
81
  desc 'watch [SECONDS]', 'Watch fan speed, load, power, and temperature ' \
70
- 'refreshed every n seconds.'
82
+ 'refreshed every n seconds.'
71
83
  def watch(seconds = 1)
72
84
  return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
73
85
 
@@ -91,12 +103,41 @@ module AmdgpuFan
91
103
  end
92
104
  end
93
105
 
106
+ desc 'watch_avg',
107
+ <<~DOC
108
+ Watch min, max, average, and current stats.
109
+ DOC
110
+ def watch_avg
111
+ puts "Watching #{amdgpu_service.name} min, max and averges since #{Time.now}...",
112
+ ' <Press Ctrl-C to exit>',
113
+ "\n\n\n\n\n"
114
+
115
+ trap 'SIGINT' do
116
+ puts "\nAnd now the watch is ended."
117
+ exit 0
118
+ end
119
+
120
+ watcher = Watcher.new amdgpu_service
121
+
122
+ loop do
123
+ watcher.measure
124
+ 5.times { print "\033[K\033[A" } # move up a line and clear to end of line
125
+
126
+ puts "#{ICONS[:clock]} Core clock #{watcher.core_clock}",
127
+ "#{ICONS[:memory]} Memory clk #{watcher.memory_clock}",
128
+ "#{ICONS[:fan]} Fan speed #{watcher.fan_speed_rpm}",
129
+ "#{ICONS[:power]} Power usage #{watcher.power_draw}",
130
+ "#{ICONS[:temp]} Temperature #{watcher.temperature}"
131
+ sleep 1
132
+ end
133
+ end
134
+
94
135
  desc 'watch_csv [SECONDS]', 'Watch stats in CSV format ' \
95
- 'refreshed every n seconds defaulting to 1 second.'
136
+ 'refreshed every n seconds defaulting to 1 second.'
96
137
  def watch_csv(seconds = 1)
97
138
  return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
98
139
 
99
- puts 'Timestamp, Core Clock (Mhz),Memory Clock (Mhz),Fan speed (rpm), '\
140
+ puts 'Timestamp,Core Clock (Mhz),Memory Clock (Mhz),Fan speed (rpm),'\
100
141
  'Load (%),Power (Watts),Temp (°C)'
101
142
 
102
143
  trap 'SIGINT' do
@@ -104,13 +145,19 @@ module AmdgpuFan
104
145
  end
105
146
 
106
147
  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(',')
148
+ list = []
149
+ threads = [
150
+ Thread.new { list[0] = amdgpu_service.core_clock },
151
+ Thread.new { list[1] = amdgpu_service.memory_clock },
152
+ Thread.new { list[2] = amdgpu_service.fan_speed_rpm },
153
+ Thread.new { list[3] = amdgpu_service.busy_percent },
154
+ Thread.new { list[4] = amdgpu_service.power_draw },
155
+ Thread.new { list[5] = amdgpu_service.temperature }
156
+ ]
157
+ threads.each(&:join)
158
+
159
+ puts [Time.now.strftime('%F %T'), *list].join(',')
160
+
114
161
  sleep seconds.to_i
115
162
  end
116
163
  end
@@ -127,7 +174,7 @@ module AmdgpuFan
127
174
 
128
175
  def fan_status
129
176
  "#{amdgpu_service.fan_mode} mode running at " \
130
- "#{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%)"
177
+ "#{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%)"
131
178
  end
132
179
 
133
180
  def mem_total_mibibyes
@@ -139,8 +186,8 @@ module AmdgpuFan
139
186
  end
140
187
 
141
188
  def summary_clock
142
- "Core: #{amdgpu_service.core_clock.rjust(7)}#{WATCH_FIELD_SEPARATOR}"\
143
- "Memory: #{amdgpu_service.memory_clock.rjust(7)}"
189
+ "Core: #{amdgpu_service.core_clock.to_s.rjust(7)}#{WATCH_FIELD_SEPARATOR}"\
190
+ "Memory: #{amdgpu_service.memory_clock.to_s.rjust(7)}"
144
191
  end
145
192
 
146
193
  def summary_fan
@@ -163,3 +210,5 @@ module AmdgpuFan
163
210
  end
164
211
  end
165
212
  end
213
+
214
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength
@@ -6,11 +6,11 @@ module AmdgpuFan
6
6
  # A model class for a GPU connector
7
7
  class Connector
8
8
  EDID_DESCRIPTORS_CONF = {
9
- display_descriptor_leading_bytes: String.new('\x00\xFC\x00', encoding: 'ascii-8bit'),
9
+ display_name_leading_bytes: String.new('\x00\xFC\x00', encoding: 'ascii-8bit'),
10
+ unspecified_text_leading_bytes: String.new('\x00\xFE\x00', encoding: 'ascii-8bit'),
10
11
  index_range: (54..125)
11
12
  }.freeze
12
13
 
13
-
14
14
  attr_reader :card_num, :dir_path, :index, :type
15
15
 
16
16
  class << self
@@ -18,11 +18,11 @@ module AmdgpuFan
18
18
  # Return an array of connector objects for the provided card number.
19
19
  # The files are sorted to improve how they are displayed to the user.
20
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,
21
+ Dir["/sys/class/drm/card#{card_num}/card#{card_num}-*"].map do |dir_path|
22
+ Connector.new card_num:,
23
+ dir_path:,
24
24
  index: dir_path[-1],
25
- type: dir_path.slice(/(?<=card#{card_num}-)[A-Z]+/)
25
+ type: dir_path.slice(/(?<=card#{card_num}-)[A-z]+/)
26
26
  end
27
27
  end
28
28
  end
@@ -41,10 +41,7 @@ module AmdgpuFan
41
41
  def display_name
42
42
  return if edid.to_s.empty?
43
43
 
44
- edid.slice(EDID_DESCRIPTORS_CONF[:index_range])
45
- .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:display_descriptor_leading_bytes]}).{1,13}/)
46
- .first
47
- .strip
44
+ (display_name_text + unspecified_text).join(' ').strip
48
45
  end
49
46
 
50
47
  def status
@@ -53,8 +50,22 @@ module AmdgpuFan
53
50
 
54
51
  private
55
52
 
53
+ def display_descriptors_raw
54
+ edid.slice EDID_DESCRIPTORS_CONF[:index_range]
55
+ end
56
+
57
+ def display_name_text
58
+ display_descriptors_raw
59
+ .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:display_name_leading_bytes]}).{1,13}/)
60
+ end
61
+
56
62
  def edid
57
63
  File.read("#{dir_path}/edid", encoding: 'ascii-8bit')
58
64
  end
65
+
66
+ def unspecified_text
67
+ display_descriptors_raw
68
+ .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:unspecified_text_leading_bytes]}).{1,13}/)
69
+ end
59
70
  end
60
71
  end
@@ -3,19 +3,35 @@
3
3
  module AmdgpuFan
4
4
  # A mixin to help with CLI output formatting
5
5
  module CliOutputFormat
6
- METER_CHAR = '*'
6
+ METER_CHARS = [' ', *("\u2588".."\u258F").to_a.reverse].freeze
7
7
  TIME_FORMAT = '%F %T'
8
8
 
9
- private
10
-
11
9
  def current_time
12
10
  Time.now.strftime(TIME_FORMAT)
13
11
  end
14
12
 
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}"
13
+ ##
14
+ # Return a string with meter and percentage for +percent+
15
+ #
16
+ def percent_meter(percent, width = 3)
17
+ meter_char_indexes = []
18
+ percent_portion_size = 100.0 / width
19
+ width.times do |i|
20
+ current_portion = percent.to_i - (percent_portion_size * i)
21
+
22
+ current_portion_percent = if current_portion >= percent_portion_size
23
+ 1
24
+ elsif current_portion <= 0
25
+ 0
26
+ else
27
+ current_portion / percent_portion_size
28
+ end
29
+
30
+ meter_char_indexes << ((METER_CHARS.length - 1) * current_portion_percent.to_f).round
31
+ end
32
+
33
+ percent_string = "#{format '%<num>0.2i', num: percent}%".ljust(4)
34
+ "#{percent_string}[#{meter_char_indexes.map { |i| METER_CHARS[i] }.join}]"
19
35
  end
20
36
 
21
37
  def radeon_logo
@@ -4,7 +4,6 @@ module AmdgpuFan
4
4
  ##
5
5
  # A mixin to read fan details and validate input
6
6
  module Fan
7
-
8
7
  private
9
8
 
10
9
  def fan_file(type)
@@ -37,7 +36,7 @@ module AmdgpuFan
37
36
  end
38
37
 
39
38
  def valid_fan_percent_speed?(percent)
40
- (1..100.to_i).cover?(percent.to_i)
39
+ (1..(100.to_i)).cover?(percent.to_i)
41
40
  end
42
41
  end
43
42
  end
@@ -3,11 +3,10 @@
3
3
  module AmdgpuFan
4
4
  # A mixin to help with writing to system files
5
5
  module SysWrite
6
-
7
6
  private
8
7
 
9
8
  ##
10
- # Write to a system file with elevated priviledges.
9
+ # Write to a system file with elevated priviledges.
11
10
  def sudo_write(file_path, value)
12
11
  `echo "#{value}" | sudo tee #{file_path}`
13
12
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
4
+
5
+ require 'net/http'
6
+
7
+ module AmdgpuFan
8
+ ## PCI
9
+ #
10
+ # Retrieve device information from PCI IDs.
11
+ # https://pci-ids.ucw.cz/
12
+ #
13
+ class PCI
14
+ TMP_FILE = '/tmp/pci.ids.txt'
15
+ URL = 'https://pci-ids.ucw.cz/v2.2/pci.ids'
16
+
17
+ ##
18
+ # Return the device name if available
19
+ #
20
+ def self.device_name(vendor_id, device_id, subdevice_id)
21
+ device = info.dig vendor_id, :devices, device_id
22
+
23
+ device.dig(subdevice_id, :name) || device[:name]
24
+ end
25
+
26
+ def self.vendor_name(vendor_id)
27
+ info.dig vendor_id, :name
28
+ end
29
+
30
+ def self.info
31
+ current_vendor_id = nil
32
+ current_device_id = nil
33
+
34
+ @info = raw.each_line.with_object({}) do |line, hash|
35
+ next if line.empty? || line.start_with?('#')
36
+
37
+ if line[0] =~ /(\d|[a-z])/
38
+ # Vendor line
39
+ current_vendor_id = line.split(' ').first.to_i(16)
40
+ vendor_name = line.split(' ').last.strip
41
+ hash[current_vendor_id] = { name: vendor_name, devices: {} }
42
+ elsif line[0..1] =~ /\t\w/
43
+ # Device line
44
+ current_device_id = line.split(' ').first.to_i(16)
45
+ device_name = line.split(' ').last.strip
46
+ hash[current_vendor_id][:devices][current_device_id] = { name: device_name }
47
+ elsif line[0..2] =~ /\t\t\w/
48
+ # Subvendor line
49
+ # subvendor_id = line.split(' ').first.to_i(16)
50
+ subdevice_id = line.split[1].to_i(16)
51
+ subdevice_name = line.split(' ')[1].strip
52
+ hash[current_vendor_id][:devices][current_device_id][subdevice_id] =
53
+ { name: subdevice_name }
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.raw
59
+ File.write(TMP_FILE, Net::HTTP.get(URI(URL))) unless File.exist?(TMP_FILE)
60
+
61
+ @raw ||= File.read(TMP_FILE)
62
+ end
63
+ end
64
+ end
65
+
66
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -4,6 +4,7 @@ require_relative 'mixin/fan'
4
4
  require_relative 'mixin/sys_write'
5
5
 
6
6
  require_relative 'connector'
7
+ require_relative 'pci'
7
8
 
8
9
  module AmdgpuFan
9
10
  ## AmdgpuService
@@ -30,21 +31,31 @@ module AmdgpuFan
30
31
  end
31
32
 
32
33
  def connectors
33
- @connectors ||= Connector.where card_num: card_num
34
+ @connectors ||= Connector.where card_num:
34
35
  end
35
36
 
36
37
  def core_clock
37
38
  clock_from_pp_file "#{base_card_dir}/pp_dpm_sclk"
38
39
  end
39
40
 
41
+ def display_names
42
+ connectors.map(&:display_name).compact
43
+ end
44
+
40
45
  def fan_mode
41
46
  FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
42
47
  end
43
48
 
49
+ ##
50
+ # Set the fan mode to auto or manual.
51
+ #
44
52
  def fan_mode=(mode)
45
53
  sudo_write fan_mode_file, FAN_MODES.key(mode.to_s)
46
54
  end
47
55
 
56
+ ##
57
+ # Set the fan speed to a percentage if <= 100 or a raw value
58
+ #
48
59
  def fan_speed=(value)
49
60
  if valid_fan_percent_speed?(value)
50
61
  new_raw = (value.to_f / 100 * fan_raw_speeds(:max).to_i).round
@@ -63,20 +74,37 @@ module AmdgpuFan
63
74
  (fan_speed_raw.to_f / fan_raw_speeds(:max).to_i * 100).round
64
75
  end
65
76
 
77
+ ##
78
+ # Return the fan speed in revolutions per minute
79
+ #
66
80
  def fan_speed_rpm
67
- File.read(fan_file(:input)).strip
81
+ File.read(fan_file(:input)).strip.to_i
68
82
  end
69
83
 
84
+ ##
85
+ # Return the current memory clock speed.
86
+ #
70
87
  def memory_clock
71
88
  clock_from_pp_file "#{base_card_dir}/pp_dpm_mclk"
72
89
  end
73
90
 
91
+ ##
92
+ # Return the total memory on the card in bytes
93
+ #
74
94
  def memory_total
75
95
  File.read("#{base_card_dir}/mem_info_vram_total").to_i
76
96
  end
77
97
 
98
+ def model_id
99
+ @model_id ||= File.read(File.join(base_card_dir, '/subsystem_device')).gsub(/\A0x/, '').upcase
100
+ end
101
+
78
102
  def name
79
- lspci_subsystem.split(': ')[1].strip
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(' ')
80
108
  end
81
109
 
82
110
  def power_dpm_state
@@ -131,15 +159,11 @@ module AmdgpuFan
131
159
  end
132
160
 
133
161
  def clock_from_pp_file(file)
134
- File.read(file).slice(/\w+(?= \*)/)
162
+ File.read(file).slice(/\w+(?= \*)/).to_i
135
163
  end
136
164
 
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:"`
165
+ def device_id
166
+ @device_id ||= File.read(File.join(base_card_dir, 'device')).to_i(16)
143
167
  end
144
168
 
145
169
  def power_avg_file
@@ -150,8 +174,16 @@ module AmdgpuFan
150
174
  (raw_string.strip.to_f / 1_000_000).round(2)
151
175
  end
152
176
 
177
+ def subdevice_id
178
+ @subdevice_id ||= File.read(File.join(base_card_dir, 'subsystem_device')).to_i(16)
179
+ end
180
+
153
181
  def temperature_file
154
182
  @temperature_file ||= Dir.glob("#{base_card_dir}/**/temp1_input").first
155
183
  end
184
+
185
+ def vendor_id
186
+ @vendor_id ||= File.read(File.join(base_card_dir, 'vendor')).to_i(16)
187
+ end
156
188
  end
157
189
  end
@@ -0,0 +1,25 @@
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
+ super
11
+ @unit = unit
12
+ end
13
+
14
+ def stats
15
+ { min: min, avg: avg, max: max, now: now }
16
+ end
17
+
18
+ ##
19
+ # Return a string containing all the stats with units.
20
+ #
21
+ def to_s
22
+ stats.map { |k, v| "#{k}: #{v.to_s.rjust(6)} #{unit.ljust(3)} " }.join
23
+ end
24
+ end
25
+ 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.8.0'
6
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+
5
+ require_relative 'stat_set'
6
+
7
+ module AmdgpuFan
8
+ # Keep track of stats over time.
9
+ class Watcher
10
+ attr_reader :core_clock, :fan_speed_rpm, :num_measurements, :memory_clock, :power_draw,
11
+ :temperature
12
+
13
+ def initialize(amdgpu_service)
14
+ @amdgpu_service = amdgpu_service
15
+ @num_measurements = 0
16
+
17
+ @core_clock = StatSet.new 'MHz'
18
+ @memory_clock = StatSet.new 'MHz'
19
+ @fan_speed_rpm = StatSet.new 'RPM'
20
+ @power_draw = StatSet.new 'W'
21
+ @temperature = StatSet.new '°C'
22
+ end
23
+
24
+ ##
25
+ # Take a new set of measurements and adjust the stats.
26
+ #
27
+ def measure
28
+ @num_measurements += 1
29
+
30
+ Async do |task|
31
+ %i[core_clock fan_speed_rpm memory_clock power_draw temperature].each do |stat|
32
+ task.async do
33
+ send(stat).now = amdgpu_service.send(stat)
34
+ calculate_stats(send(stat))
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :amdgpu_service
43
+
44
+ def calculate_stats(stat_set)
45
+ if num_measurements == 1
46
+ stat_set.min = stat_set.now
47
+ stat_set.avg = stat_set.now.to_f
48
+ stat_set.max = stat_set.now
49
+ return
50
+ end
51
+
52
+ stat_set.min = stat_set.now if stat_set.now < stat_set.min
53
+ stat_set.avg =
54
+ ((stat_set.now + (stat_set.avg * (num_measurements - 1))) / num_measurements.to_f)
55
+ .round(1)
56
+ stat_set.max = stat_set.now if stat_set.now > stat_set.max
57
+ end
58
+ end
59
+ end
metadata CHANGED
@@ -1,15 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amdgpu_fan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.8.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: 2020-03-15 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-01-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
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
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.1'
13
97
  description: A CLI for interacting with the amdgpu Linux driver
14
98
  email: harlemsquirrel@gmail.com
15
99
  executables:
@@ -20,18 +104,24 @@ files:
20
104
  - README.md
21
105
  - bin/amdgpu_fan
22
106
  - config/environment.rb
107
+ - config/icons.yml
23
108
  - lib/amdgpu_fan.rb
24
109
  - lib/amdgpu_fan/cli.rb
25
110
  - lib/amdgpu_fan/connector.rb
26
111
  - lib/amdgpu_fan/mixin/cli_output_format.rb
27
112
  - lib/amdgpu_fan/mixin/fan.rb
28
113
  - lib/amdgpu_fan/mixin/sys_write.rb
114
+ - lib/amdgpu_fan/pci.rb
29
115
  - lib/amdgpu_fan/service.rb
116
+ - lib/amdgpu_fan/stat_set.rb
117
+ - lib/amdgpu_fan/version.rb
118
+ - lib/amdgpu_fan/watcher.rb
30
119
  homepage: https://github.com/HarlemSquirrel/amdgpu-fan-rb
31
120
  licenses:
32
121
  - MIT
33
- metadata: {}
34
- post_install_message:
122
+ metadata:
123
+ rubygems_mfa_required: 'true'
124
+ post_install_message:
35
125
  rdoc_options: []
36
126
  require_paths:
37
127
  - lib
@@ -39,15 +129,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
129
  requirements:
40
130
  - - ">="
41
131
  - !ruby/object:Gem::Version
42
- version: '0'
132
+ version: 3.1.0
43
133
  required_rubygems_version: !ruby/object:Gem::Requirement
44
134
  requirements:
45
135
  - - ">="
46
136
  - !ruby/object:Gem::Version
47
137
  version: '0'
48
138
  requirements: []
49
- rubygems_version: 3.1.2
50
- signing_key:
139
+ rubygems_version: 3.3.3
140
+ signing_key:
51
141
  specification_version: 4
52
142
  summary: A CLI to view and set fan speeds for AMD graphics cards running on the open
53
143
  source amdgpu Linux driver