amdgpu_fan 0.2.0 → 0.6.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: 474b8500f7505a2793cf2a3cc1345d39273c78b2749cde1a40bef0f8050910e0
4
- data.tar.gz: ceb407543897bd36b851c1c1c42660e8f146878cd354ee0411a104b9358fb133
3
+ metadata.gz: 7f57ebc2011d08efe82baa67964bf75a17dfe7c85a518249a93c7b6eeadf207e
4
+ data.tar.gz: 1e1fa6d5af4f3cecc5346003949eb4a1edeb1c1ef943e60478f09cd8e3600099
5
5
  SHA512:
6
- metadata.gz: 2808b9d2b5ce15880b9ee3937bac0f534c81b66c3824fe44439ebfc7449ba52d7ad44e930cc21f201bedd1bc6b34a3d17f3d469aed62a534fc8e6cc005064a09
7
- data.tar.gz: 2ddc2aeedb9648308e7bf39f7421d5ae5397addd9df7ed35e6f15e903c78b8c36c946df1a48e529e2cdd2de7629696e0e7caa67d0923d65d9b2b72f56aad7bcd
6
+ metadata.gz: a185688066129ae01860f6dcf15fa75d84b5441f0cf3fbf23df812986c8da194eb6ed00557afe607299d6d3640beed3b5dc60483b1e6155d09d4e40180865793
7
+ data.tar.gz: 2ef75cd3c949fd579eaf855c63ca985a1ddff172e41fceb46dd10b5f1177e0ddaf7f74427e3ce64c26a86ac15da473806f400014abaf428fb7275f075cd79069
data/README.md CHANGED
@@ -1,33 +1,104 @@
1
- # amdgpu-fan-rb
1
+ # amdgpu_fan
2
2
 
3
- A Ruby CLI to read and set the fan speed for AMD Radeon graphics cards running on the AMDGPU Linux driver.
3
+ [![Gem Version](https://badge.fury.io/rb/amdgpu_fan.svg)](https://badge.fury.io/rb/amdgpu_fan)
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
- See https://wiki.archlinux.org/index.php/Fan_speed_control#AMDGPU_sysfs_fan_control
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.
6
7
 
7
- ## Usage
8
+ **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.
9
+
10
+ #### Further reading
11
+
12
+ - https://wiki.archlinux.org/index.php/AMDGPU#Overclocking
13
+ - https://wiki.archlinux.org/index.php/Fan_speed_control#AMDGPU_sysfs_fan_control
14
+ - https://phoronix.com/scan.php?page=news_item&px=AMDGPU-Quick-WattMan-Cap-Test
15
+
16
+ ## Installation
17
+
18
+ The `amdgpu_fan` CLI command can be installed from [RubyGems](https://rubygems.org/gems/amdgpu_fan) or easily run from the source code.
19
+
20
+ ### From RubyGems
8
21
 
9
22
  ```
10
- ➤ bundle
23
+ gem install amdgpu_fan
24
+ ```
25
+
26
+ ### From Source
27
+
28
+ ```
29
+ ➤ git clone https://github.com/HarlemSquirrel/amdgpu-fan-rb.git
30
+ ➤ cd amdgpu-fan-rb
31
+ ➤ bundle install
32
+ ➤ bin/amdgpu_fan
33
+ ```
11
34
 
35
+ ## Usage
36
+
37
+ ```
12
38
  ➤ bin/amdgpu_fan help
13
39
  Commands:
14
- amdgpu_fan auto # Set mode to automatic (requires sudo)
15
- amdgpu_fan help [COMMAND] # Describe available commands or one specific command
16
- amdgpu_fan set PERCENTAGE # Set fan speed to PERCENTAGE (requires sudo)
17
- amdgpu_fan status # View device info, current fan speed, and temperature
18
- amdgpu_fan watch [SECONDS] # Watch current fan speed, and temperature refreshed every n seconds
40
+ amdgpu_fan connectors # View the status of the display connectors.
41
+ amdgpu_fan fan # View fan details.
42
+ amdgpu_fan fan_set PERCENTAGE/AUTO # Set fan speed to percentage or automatic mode. (requires sudo)
43
+ amdgpu_fan help [COMMAND] # Describe available commands or one specific command
44
+ amdgpu_fan profile # View power profile details.
45
+ amdgpu_fan profile_auto # Set the power profile to automatic mode.
46
+ amdgpu_fan profile_force PROFILE_NUM # Manually set a power profile. (requires sudo)
47
+ amdgpu_fan status [--logo] # View device info, current fan speed, and temperature.
48
+ amdgpu_fan watch [SECONDS] # Watch fan speed, load, power, and temperature refreshed every n seconds.
49
+ amdgpu_fan watch_csv [SECONDS] # Watch stats in CSV format refreshed every n seconds defaulting to 1 second.
19
50
 
20
51
  ➤ bin/amdgpu_fan status
21
- 📺 GPU: AMD Radeon (TM) R9 Fury Series
22
- 📄 vBIOS: 113-C8800100-102
23
- 🌀 Fan: auto mode running at 48% ~ 1828 rpm
24
- 🌡 Temp: 28.0°C
25
- ⚡ Power: 19.26 / 300.0 Watts
52
+ 📺 GPU: Advanced Micro Devices, Inc. [AMD/ATI] Radeon R9 FURY X / NANO
53
+ 📄 vBIOS: 113-C8800100-102
54
+ ⏰ Clocks: 724Mhz Core, 500Mhz Memory
55
+ 💾 Memory: 4096 MiB
56
+ 🌀 Fan: auto mode running at 1809 rpm (48%)
57
+ 🌞 Temp: 21.0°C
58
+ ⚡ Power: 3D_FULL_SCREEN profile in performance mode using 16.2 / 300.0 Watts (5%)
59
+ ⚖ Load: [ ]0%
60
+
61
+ ➤ bin/amdgpu_fan watch 3
62
+ Watching Advanced Micro Devices, Inc. [AMD/ATI] Radeon R9 FURY X / NANO every 3 second(s)...
63
+ <Press Ctrl-C to exit>
64
+ 2019-05-28 20:57:41 | Core: 724Mhz | Memory: 500Mhz | Fan: 948 rpm [* ]14% | Load: [** ]24% | Power: 16.07 W [* ]6% | Temp: 34.0°C
65
+ 2019-05-28 20:57:45 | Core: 512Mhz | Memory: 500Mhz | Fan: 948 rpm [* ]14% | Load: [ ]0% | Power: 16.13 W [* ]7% | Temp: 34.0°C
66
+ 2019-05-28 20:57:49 | Core: 892Mhz | Memory: 500Mhz | Fan: 948 rpm [* ]14% | Load: [ ]0% | Power: 25.22 W [* ]5% | Temp: 33.0°C
67
+ 2019-05-28 20:57:53 | Core: 300Mhz | Memory: 500Mhz | Fan: 948 rpm [* ]14% | Load: [ ]0% | Power: 19.1 W [* ]6% | Temp: 33.0°C
68
+ 2019-05-28 20:57:57 | Core: 1050Mhz | Memory: 500Mhz | Fan: 948 rpm [* ]14% | Load: [********* ]94% | Power: 103.04 W [*** ]31% | Temp: 36.0°C
69
+ 2019-05-28 20:58:01 | Core: 1050Mhz | Memory: 500Mhz | Fan: 954 rpm [** ]15% | Load: [********* ]91% | Power: 158.07 W [***** ]53% | Temp: 38.0°C
70
+ 2019-05-28 20:58:05 | Core: 1050Mhz | Memory: 500Mhz | Fan: 977 rpm [** ]16% | Load: [**********]100% | Power: 218.01 W [******* ]73% | Temp: 40.0°C
71
+ 2019-05-28 20:58:09 | Core: 1050Mhz | Memory: 500Mhz | Fan: 1005 rpm [** ]16% | Load: [**********]100% | Power: 216.24 W [******* ]71% | Temp: 40.0°C
72
+ 2019-05-28 20:58:13 | Core: 1050Mhz | Memory: 500Mhz | Fan: 1033 rpm [** ]17% | Load: [**********]97% | Power: 109.25 W [**** ]39% | Temp: 38.0°C
73
+ 2019-05-28 20:58:17 | Core: 724Mhz | Memory: 500Mhz | Fan: 1058 rpm [** ]17% | Load: [ ]0% | Power: 17.17 W [* ]6% | Temp: 35.0°C
74
+ ^C
75
+ And now the watch is ended.
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.
26
90
  ```
27
91
 
28
92
  ## Dependencies
29
93
 
30
- - Ruby
31
- - bundler
32
- - thor (installed with bundler)
33
- - lspci (included with most Linux distributions)
94
+ - [Ruby](https://www.ruby-lang.org) with [Bundler](https://bundler.io)
95
+ - [Thor](http://whatisthor.com/) (installed with `bundle install`)
96
+ - [`lspci`](https://linux.die.net/man/8/lspci) (included with most Linux distributions)
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,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
@@ -1,3 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
 
3
- require_relative '../lib/amdgpu_service'
5
+ require_relative '../lib/amdgpu_fan'
6
+ require_relative '../lib/amdgpu_fan/version'
7
+
8
+ require_relative '../lib/amdgpu_fan/mixin/cli_output_format'
9
+ require_relative '../lib/amdgpu_fan/mixin/sys_write'
10
+
11
+ require_relative '../lib/amdgpu_fan/service'
12
+ require_relative '../lib/amdgpu_fan/cli'
13
+ require_relative '../lib/amdgpu_fan/stat_set'
14
+ require_relative '../lib/amdgpu_fan/watcher'
@@ -0,0 +1,10 @@
1
+ ---
2
+ clock: ⏰
3
+ display: 📺
4
+ fan: 🌀
5
+ gpu: 👾
6
+ load: "⚖ "
7
+ memory: 💾
8
+ power: ⚡
9
+ temp: "🌡 "
10
+ vbios: 📄
@@ -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,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module AmdgpuFan
6
+ # The command-line interface class
7
+ class Cli < Thor
8
+ include CliOutputFormat
9
+
10
+ ICONS = YAML.load(File.read(File.join(__dir__, '../../config/icons.yml')))
11
+ .transform_keys(&:to_sym).freeze
12
+ WATCH_FIELD_SEPARATOR = ' | '
13
+
14
+ desc 'connectors', 'View the status of the display connectors.'
15
+ def connectors
16
+ amdgpu_service.connectors.each do |connector|
17
+ puts "#{connector.type} #{connector.index}:\t" +
18
+ (connector.connected? ? connector.display_name : connector.status)
19
+ end
20
+ end
21
+
22
+ desc 'profile', 'View power profile details.'
23
+ def profile
24
+ puts amdgpu_service.profile_summary
25
+ end
26
+
27
+ desc 'profile_auto', 'Set the power profile to automatic mode.'
28
+ def profile_auto
29
+ amdgpu_service.profile_auto
30
+ puts amdgpu_service.profile_summary
31
+ end
32
+
33
+ desc 'profile_force PROFILE_NUM', 'Manually set a power profile. (requires sudo)'
34
+ def profile_force(state)
35
+ amdgpu_service.profile_force = state
36
+ puts amdgpu_service.profile_summary
37
+ end
38
+
39
+ desc 'fan', 'View fan details.'
40
+ def fan
41
+ puts fan_status
42
+ end
43
+
44
+ desc 'fan_set PERCENTAGE/AUTO', 'Set fan speed to percentage or automatic mode. (requires sudo)'
45
+ def fan_set(value)
46
+ if value.strip.casecmp('auto').zero?
47
+ amdgpu_service.fan_mode = :auto
48
+ else
49
+ return puts 'Invalid percentage' unless (0..100).cover?(value.to_i)
50
+
51
+ amdgpu_service.fan_speed = value
52
+ end
53
+ puts fan_status
54
+ end
55
+
56
+ desc 'status [--logo]', 'View device info, current fan speed, and temperature.'
57
+ def status(option = nil)
58
+ puts radeon_logo if option == '--logo'
59
+ puts ICONS[:gpu] + ' GPU:'.ljust(9) + amdgpu_service.name,
60
+ ICONS[:vbios]+ ' vBIOS:'.ljust(9) + amdgpu_service.vbios_version,
61
+ ICONS[:display] + ' Displays:' + amdgpu_service.display_names.join(', '),
62
+ ICONS[:clock] + ' Clocks:'.ljust(9) + clock_status,
63
+ ICONS[:memory] + ' Memory:'.ljust(9) + mem_total_mibibyes,
64
+ ICONS[:fan] + ' Fan:'.ljust(9) + fan_status,
65
+ ICONS[:temp] + ' Temp:'.ljust(9) + "#{amdgpu_service.temperature}°C",
66
+ ICONS[:power] + ' Power:'.ljust(9) + "#{amdgpu_service.profile_mode} profile in " \
67
+ "#{amdgpu_service.power_dpm_state} mode using " \
68
+ "#{amdgpu_service.power_draw} / #{amdgpu_service.power_max} Watts "\
69
+ "(#{amdgpu_service.power_draw_percent}%)",
70
+ ICONS[:load] + ' Load:'.ljust(9) + percent_meter(amdgpu_service.busy_percent, 20)
71
+ end
72
+
73
+ desc 'version', 'Print the application version.'
74
+ def version
75
+ puts AmdgpuFan::VERSION
76
+ end
77
+
78
+ desc 'watch [SECONDS]', 'Watch fan speed, load, power, and temperature ' \
79
+ 'refreshed every n seconds.'
80
+ def watch(seconds = 1)
81
+ return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
82
+
83
+ puts "Watching #{amdgpu_service.name} every #{seconds} second(s)...",
84
+ ' <Press Ctrl-C to exit>'
85
+
86
+ trap 'SIGINT' do
87
+ puts "\nAnd now the watch is ended."
88
+ exit 0
89
+ end
90
+
91
+ loop do
92
+ time = Time.now
93
+ puts [time.strftime('%F %T'), summary_clock, summary_fan, summary_load, summary_power,
94
+ summary_temp].join(WATCH_FIELD_SEPARATOR)
95
+
96
+ # It can take a second or two to run the above so we remove them from the wait
97
+ # here to get a more consistant watch interval.
98
+ sec_left_to_wait = time.to_i + seconds.to_i - Time.now.to_i
99
+ sleep sec_left_to_wait if sec_left_to_wait.positive?
100
+ end
101
+ end
102
+
103
+ desc 'watch_avg',
104
+ <<~DOC
105
+ Watch min, max, average, and current stats.
106
+ DOC
107
+ def watch_avg
108
+ puts "Watching #{amdgpu_service.name} min, max and averges since #{Time.now}...",
109
+ ' <Press Ctrl-C to exit>',
110
+ "\n\n\n\n\n"
111
+
112
+ trap 'SIGINT' do
113
+ puts "\nAnd now the watch is ended."
114
+ exit 0
115
+ end
116
+
117
+ watcher = Watcher.new amdgpu_service
118
+
119
+ loop do
120
+ watcher.measure
121
+ 5.times { print "\033[K\033[A" } # move up a line and clear to end of line
122
+
123
+ puts ICONS[:clock] + ' Core clock ' + watcher.core_clock.to_s,
124
+ ICONS[:memory] + ' Memory clk ' + watcher.mem_clock.to_s,
125
+ ICONS[:fan] + ' Fan speed ' + watcher.fan_speed.to_s,
126
+ ICONS[:power] + ' Power usage ' + watcher.power.to_s,
127
+ ICONS[:temp] + ' Temperature ' + watcher.temp.to_s
128
+ sleep 1
129
+ end
130
+ end
131
+
132
+ desc 'watch_csv [SECONDS]', 'Watch stats in CSV format ' \
133
+ 'refreshed every n seconds defaulting to 1 second.'
134
+ def watch_csv(seconds = 1)
135
+ return puts 'Seconds must be from 1 to 600' unless (1..600).cover?(seconds.to_i)
136
+
137
+ puts 'Timestamp, Core Clock (Mhz),Memory Clock (Mhz),Fan speed (rpm), '\
138
+ 'Load (%),Power (Watts),Temp (°C)'
139
+
140
+ trap 'SIGINT' do
141
+ exit 0
142
+ end
143
+
144
+ loop do
145
+ puts [Time.now.strftime('%F %T'),
146
+ amdgpu_service.core_clock,
147
+ amdgpu_service.memory_clock,
148
+ amdgpu_service.fan_speed_rpm,
149
+ amdgpu_service.busy_percent,
150
+ amdgpu_service.power_draw,
151
+ amdgpu_service.temperature].join(',')
152
+ sleep seconds.to_i
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def amdgpu_service
159
+ @amdgpu_service ||= AmdgpuFan::Service.new
160
+ end
161
+
162
+ def clock_status
163
+ "#{amdgpu_service.core_clock} Core, #{amdgpu_service.memory_clock} Memory"
164
+ end
165
+
166
+ def fan_status
167
+ "#{amdgpu_service.fan_mode} mode running at " \
168
+ "#{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%)"
169
+ end
170
+
171
+ def mem_total_mibibyes
172
+ "#{amdgpu_service.memory_total / (2**20)} MiB"
173
+ end
174
+
175
+ def power_max
176
+ format('%<num>0.2f', num: amdgpu_service.power_max)
177
+ end
178
+
179
+ def summary_clock
180
+ "Core: #{amdgpu_service.core_clock.rjust(7)}#{WATCH_FIELD_SEPARATOR}"\
181
+ "Memory: #{amdgpu_service.memory_clock.rjust(7)}"
182
+ end
183
+
184
+ def summary_fan
185
+ fan_speed_string = "#{amdgpu_service.fan_speed_rpm} rpm".rjust(8)
186
+ "Fan: #{fan_speed_string} #{percent_meter(amdgpu_service.fan_speed_percent)}"
187
+ end
188
+
189
+ def summary_load
190
+ "Load: #{percent_meter amdgpu_service.busy_percent}"
191
+ end
192
+
193
+ def summary_power
194
+ "Power: #{format('%<num>0.02f', num: amdgpu_service.power_draw).rjust(power_max.length)} W" \
195
+ " #{percent_meter amdgpu_service.power_draw_percent}"
196
+ end
197
+
198
+ def summary_temp
199
+ temp_string = "#{amdgpu_service.temperature}°C".rjust(7)
200
+ "Temp: #{temp_string}"
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,72 @@
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_name_leading_bytes: String.new('\x00\xFC\x00', encoding: 'ascii-8bit'),
10
+ unspecified_text_leading_bytes: String.new('\x00\xFE\x00', encoding: 'ascii-8bit'),
11
+ index_range: (54..125)
12
+ }.freeze
13
+
14
+
15
+ attr_reader :card_num, :dir_path, :index, :type
16
+
17
+ class << self
18
+ ##
19
+ # Return an array of connector objects for the provided card number.
20
+ # The files are sorted to improve how they are displayed to the user.
21
+ def where(card_num:)
22
+ Dir["/sys/class/drm/card#{card_num}/card#{card_num}-*"].sort.map do |dir_path|
23
+ Connector.new card_num: card_num,
24
+ dir_path: dir_path,
25
+ index: dir_path[-1],
26
+ type: dir_path.slice(/(?<=card#{card_num}-)[A-z]+/)
27
+ end
28
+ end
29
+ end
30
+
31
+ def initialize(card_num:, dir_path:, index:, type:)
32
+ @card_num = card_num
33
+ @dir_path = dir_path
34
+ @index = index
35
+ @type = type
36
+ end
37
+
38
+ def connected?
39
+ status.casecmp('connected').zero?
40
+ end
41
+
42
+ def display_name
43
+ return if edid.to_s.empty?
44
+
45
+ (display_name_text + unspecified_text).join(' ').strip
46
+ end
47
+
48
+ def status
49
+ File.read(File.join(dir_path, 'status')).strip
50
+ end
51
+
52
+ private
53
+
54
+ def display_descriptors_raw
55
+ edid.slice EDID_DESCRIPTORS_CONF[:index_range]
56
+ end
57
+
58
+ def display_name_text
59
+ display_descriptors_raw
60
+ .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:display_name_leading_bytes]}).{1,13}/)
61
+ end
62
+
63
+ def edid
64
+ File.read("#{dir_path}/edid", encoding: 'ascii-8bit')
65
+ end
66
+
67
+ def unspecified_text
68
+ display_descriptors_raw
69
+ .scan(/(?<=#{EDID_DESCRIPTORS_CONF[:unspecified_text_leading_bytes]}).{1,13}/)
70
+ end
71
+ end
72
+ 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,161 @@
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 display_names
41
+ connectors.map(&:display_name).compact
42
+ end
43
+
44
+ def fan_mode
45
+ FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
46
+ end
47
+
48
+ def fan_mode=(mode)
49
+ sudo_write fan_mode_file, FAN_MODES.key(mode.to_s)
50
+ end
51
+
52
+ def fan_speed=(value)
53
+ if valid_fan_percent_speed?(value)
54
+ new_raw = (value.to_f / 100 * fan_raw_speeds(:max).to_i).round
55
+ elsif valid_fan_raw_speed?(value)
56
+ new_raw = value
57
+ end
58
+
59
+ raise(self.class::Error, 'Invalid fan speed provided') if new_raw.to_s.empty?
60
+
61
+ self.fan_mode = :manual unless fan_mode == 'manual'
62
+
63
+ sudo_write fan_power_file, new_raw
64
+ end
65
+
66
+ def fan_speed_percent
67
+ (fan_speed_raw.to_f / fan_raw_speeds(:max).to_i * 100).round
68
+ end
69
+
70
+ def fan_speed_rpm
71
+ File.read(fan_file(:input)).strip
72
+ end
73
+
74
+ def memory_clock
75
+ clock_from_pp_file "#{base_card_dir}/pp_dpm_mclk"
76
+ end
77
+
78
+ def memory_total
79
+ File.read("#{base_card_dir}/mem_info_vram_total").to_i
80
+ end
81
+
82
+ def name
83
+ lspci_subsystem.split(': ')[1].strip
84
+ end
85
+
86
+ def power_dpm_state
87
+ File.read("#{base_card_dir}/power_dpm_state").strip
88
+ end
89
+
90
+ def power_draw
91
+ power_raw_to_watts File.read(power_avg_file)
92
+ end
93
+
94
+ def power_draw_percent
95
+ (power_draw.to_f / power_max.to_i * 100).round
96
+ end
97
+
98
+ def power_max
99
+ @power_max ||= power_raw_to_watts File.read("#{base_hwmon_dir}/power1_cap")
100
+ end
101
+
102
+ def profile_auto
103
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'auto'
104
+ end
105
+
106
+ def profile_force=(state)
107
+ sudo_write "#{base_card_dir}/power_dpm_force_performance_level", 'manual'
108
+ sudo_write "#{base_card_dir}/pp_power_profile_mode", state
109
+ end
110
+
111
+ def profile_mode
112
+ File.read("#{base_card_dir}/pp_power_profile_mode").slice(/\w+\s*+\*/).delete('*').strip
113
+ end
114
+
115
+ def profile_summary
116
+ File.read("#{base_card_dir}/pp_power_profile_mode")
117
+ end
118
+
119
+ def temperature
120
+ (File.read(temperature_file).to_f / 1000).round(1)
121
+ end
122
+
123
+ def vbios_version
124
+ @vbios_version ||= File.read("#{base_card_dir}/vbios_version").strip
125
+ end
126
+
127
+ private
128
+
129
+ def base_card_dir
130
+ @base_card_dir ||= "#{BASE_FOLDER}/card#{card_num}/device"
131
+ end
132
+
133
+ def base_hwmon_dir
134
+ @base_hwmon_dir ||= Dir.glob("#{base_card_dir}/hwmon/hwmon*").first
135
+ end
136
+
137
+ def clock_from_pp_file(file)
138
+ File.read(file).slice(/\w+(?= \*)/)
139
+ end
140
+
141
+ def gpu_pci_id
142
+ @gpu_pci_id ||= `lspci -v | grep VGA`.split(' ').first
143
+ end
144
+
145
+ def lspci_subsystem
146
+ @lspci_subsystem ||= `lspci -v -s #{gpu_pci_id} | grep "Subsystem:"`
147
+ end
148
+
149
+ def power_avg_file
150
+ @power_avg_file ||= Dir.glob("#{base_card_dir}/**/power1_average").first
151
+ end
152
+
153
+ def power_raw_to_watts(raw_string)
154
+ (raw_string.strip.to_f / 1_000_000).round(2)
155
+ end
156
+
157
+ def temperature_file
158
+ @temperature_file ||= Dir.glob("#{base_card_dir}/**/temp1_input").first
159
+ end
160
+ end
161
+ 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.6.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.2.0
4
+ version: 0.6.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-01-14 00:00:00.000000000 Z
12
- dependencies: []
13
- description: A CLI for amdgpu fans
11
+ date: 2020-06-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,9 +62,17 @@ 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/service.rb
73
+ - lib/amdgpu_fan/stat_set.rb
74
+ - lib/amdgpu_fan/version.rb
75
+ - lib/amdgpu_fan/watcher.rb
26
76
  homepage: https://github.com/HarlemSquirrel/amdgpu-fan-rb
27
77
  licenses:
28
78
  - MIT
@@ -42,8 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
42
92
  - !ruby/object:Gem::Version
43
93
  version: '0'
44
94
  requirements: []
45
- rubyforge_project:
46
- rubygems_version: 2.7.7
95
+ rubygems_version: 3.1.2
47
96
  signing_key:
48
97
  specification_version: 4
49
98
  summary: A CLI to view and set fan speeds for AMD graphics cards running on the open
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../config/environment'
4
-
5
- # The main class
6
- class AmdgpuFanCli < Thor
7
- desc 'auto', 'Set mode to automatic (requires sudo)'
8
- def auto
9
- amdgpu_service.set_fan_mode! :auto
10
- puts fan_status
11
- end
12
-
13
- desc 'set PERCENTAGE', 'Set fan speed to PERCENTAGE (requires sudo)'
14
- def set(percentage)
15
- return puts "Invalid percentage" unless (0..100).cover?(percentage.to_i)
16
-
17
- amdgpu_service.set_fan_manual_speed! percent: percentage
18
- puts fan_status
19
- rescue AmdgpuService::Error
20
- puts 'Invalid fan speed provided. The percentage should be between 1 and 100'
21
- exit 1
22
- end
23
-
24
- desc 'status', 'View device info, current fan speed, and temperature'
25
- def status
26
- print_radeon_logo
27
- puts "📺\tGPU: #{amdgpu_service.name}",
28
- "📄\tvBIOS: #{amdgpu_service.vbios_version}",
29
- fan_status,
30
- "🌡\tTemp: #{amdgpu_service.temperature}°C",
31
- "⚡\tPower: #{amdgpu_service.power_dpm_state} mode using " \
32
- "#{amdgpu_service.power_draw} / #{amdgpu_service.power_max} Watts",
33
- "⚖\tLoad: #{amdgpu_service.busy_percent}%"
34
- end
35
-
36
- desc 'watch [SECONDS]', 'Watch fan speed, load, power, and temperature ' \
37
- 'refreshed every n seconds'
38
- def watch(seconds=1)
39
- return puts "Seconds must be from 1 to 600" unless (1..600).cover?(seconds.to_i)
40
-
41
- puts "Watching #{amdgpu_service.name} every #{seconds} second(s)...",
42
- ' <Press Ctrl-C to exit>'
43
-
44
- trap "SIGINT" do
45
- puts 'And now the watch is ended.'
46
- exit 0
47
- end
48
-
49
- loop do
50
- puts "#{Time.now.strftime("%F %T")} " \
51
- "Fan: #{amdgpu_service.fan_speed_rpm} rpm (#{amdgpu_service.fan_speed_percent}%), " \
52
- "Load: #{amdgpu_service.busy_percent}%, " \
53
- "Power: #{amdgpu_service.power_draw} W, " \
54
- "Temp: #{amdgpu_service.temperature}°C "
55
- sleep seconds.to_i
56
- end
57
- end
58
-
59
- private
60
-
61
- def amdgpu_service
62
- @amdgpu_service ||= AmdgpuService.new
63
- end
64
-
65
- def current_time
66
- Time.now.strftime("%F %T")
67
- end
68
-
69
- def fan_status
70
- "🌀\tFan: #{amdgpu_service.fan_mode} mode running at " \
71
- "#{amdgpu_service.fan_speed_percent}% ~ #{amdgpu_service.fan_speed_rpm} rpm"
72
- end
73
-
74
- def print_radeon_logo
75
- puts File.read('lib/radeon_r_black_red_100x100.ascii')
76
- end
77
- end
@@ -1,134 +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 fan_mode
24
- FAN_MODES[File.read(fan_mode_file).strip] || 'unknown'
25
- end
26
-
27
- def set_fan_mode!(mode)
28
- `echo "#{FAN_MODES.key(mode.to_s)}" | sudo tee #{fan_mode_file}`
29
- end
30
-
31
- def fan_speed_percent
32
- (fan_speed_raw.to_f / fan_speed_raw_max.to_i * 100).round
33
- end
34
-
35
- def fan_speed_raw_max
36
- @fan_speed_raw_max ||= File.read(Dir.glob("#{base_card_folder}/**/pwm1_max").first).strip
37
- end
38
-
39
- def fan_speed_rpm
40
- File.read(fan_input_file).strip
41
- end
42
-
43
- def name
44
- lspci_subsystem.split(': ')[1].strip
45
- end
46
-
47
- def power_dpm_state
48
- File.read("#{base_card_folder}/power_dpm_state").strip
49
- end
50
-
51
- def power_draw
52
- power_raw_to_watts File.read(power_avg_file)
53
- end
54
-
55
- def power_max
56
- @power_max ||= power_raw_to_watts File.read(power_max_file)
57
- end
58
-
59
- def set_fan_manual_speed!(percent: nil, raw: nil)
60
- if valid_fan_percent_speed?(percent)
61
- new_raw = (percent.to_f / 100 * fan_speed_raw_max.to_i).round
62
- elsif valid_fan_raw_speed?(raw)
63
- new_raw = raw
64
- end
65
-
66
- raise(self.class::Error, 'Invalid fan speed provided') if new_raw.to_s.empty?
67
-
68
- set_fan_mode!(:manual) unless fan_mode == 'manual'
69
-
70
- `echo "#{new_raw}" | sudo tee #{fan_power_file}`
71
- end
72
-
73
- def temperature
74
- (File.read(temperature_file).to_f / 1000).round(1)
75
- end
76
-
77
- def vbios_version
78
- @vbios_version ||= File.read("#{base_card_folder}/vbios_version").strip
79
- end
80
-
81
- private
82
-
83
- def base_card_folder
84
- @base_card_folder ||= "#{BASE_FOLDER}/card#{card_num}/device"
85
- end
86
-
87
- def fan_input_file
88
- @fan_input_file ||= Dir.glob("#{base_card_folder}/**/fan1_input").first
89
- end
90
-
91
- def fan_mode_file
92
- @fan_mode_file ||= Dir.glob("#{base_card_folder}/**/pwm1_enable").first
93
- end
94
-
95
- def fan_power_file
96
- @fan_power_file ||= Dir.glob("#{base_card_folder}/**/pwm1").first
97
- end
98
-
99
- def fan_speed_raw
100
- File.read(fan_power_file).strip
101
- end
102
-
103
- def gpu_pci_id
104
- @gpu_pci_id ||= `lspci -v | grep VGA`.split(' ').first
105
- end
106
-
107
- def lspci_subsystem
108
- @lspci_subsystem ||= `lspci -v -s #{gpu_pci_id} | grep "Subsystem:"`
109
- end
110
-
111
- def power_avg_file
112
- @power_avg_file ||= Dir.glob("#{base_card_folder}/**/power1_average").first
113
- end
114
-
115
- def power_max_file
116
- @power_avg_file ||= Dir.glob("#{base_card_folder}/**/power1_cap").first
117
- end
118
-
119
- def power_raw_to_watts(raw_string)
120
- (raw_string.strip.to_f / 1_000_000).round(2)
121
- end
122
-
123
- def temperature_file
124
- @temperature_file ||= Dir.glob("#{base_card_folder}/**/temp1_input").first
125
- end
126
-
127
- def valid_fan_raw_speed?(raw)
128
- (1..fan_speed_raw_max.to_i).include?(raw.to_i)
129
- end
130
-
131
- def valid_fan_percent_speed?(percent)
132
- (1..100.to_i).include?(percent.to_i)
133
- end
134
- 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
- ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''