diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..06538ff --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,20 @@ +name: Ruby +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.7' + - name: Build and test with Rake + run: | + gem install bundler + bundle install --jobs 4 --retry 3 + bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd54a94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +*.gem diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..fd3a646 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,17 @@ +Style/Documentation: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/MethodLength: + Max: 25 + +Metrics/CyclomaticComplexity: + Max: 10 + +Style/PerlBackrefs: + Enabled: false diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b1c4c0d --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'minitest', '~> 5.21.2' +gem 'rake', '~> 13.0.0' +gem 'rubocop', '~> 1.60' + +gem 'open3' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..106af86 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,44 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.7.1) + language_server-protocol (3.17.0.3) + minitest (5.21.2) + open3 (0.1.2) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + rake (13.0.6) + regexp_parser (2.9.0) + rexml (3.2.6) + rubocop (1.60.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + minitest (~> 5.21.2) + open3 + rake (~> 13.0.0) + rubocop (~> 1.60) + +BUNDLED WITH + 2.4.10 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b9a156 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# SenseThing: A sensor tool for Linux + +`sensething` is a CLI tool that aims to: + +- Make as many sensors as possible visible from within a single tool, +- Provide a human-friendly way of querying specific sensors, +- Deliver output in formats that are easy to parse (for humans and computers), +- Be as simple to use as possible. + +`sensething` provides access to all the same sensors as `lm-sensors`, but it +also includes sensor data from Nvidia graphics cards, as well as CPU clock +frequencies. It will provide access to even more sensors in the future. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ecb65c7 --- /dev/null +++ b/Rakefile @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rubocop/rake_task' + +GEM_NAME = 'sensething' +GEM_VERSION = '0.0.1' + +def del(pattern) + pattern = File.join(__dir__, pattern) + Dir.glob(pattern).each do |f| + File.delete f + end +end + +task default: %i[build test] + +RuboCop::RakeTask.new(:lint) do |task| + task.patterns = ['lib/**/*.rb', 'test/**/*.rb'] + task.fail_on_error = true +end + +task :build do + system "gem build #{GEM_NAME} .gemspec" +end + +task install: :build do + system "gem install #{GEM_NAME}-#{GEM_VERSION}.gem" +end + +task publish: :build do + system "gem push #{GEM_NAME}-#{GEM_VERSION}.gem" +end + +task :test do + Dir.glob('test/*.rb').each do |f| + ruby f + end +end + +task :clean do + del '*.gem' +end diff --git a/bin/sensething b/bin/sensething new file mode 100755 index 0000000..48a67e1 --- /dev/null +++ b/bin/sensething @@ -0,0 +1,126 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'sensething' +require 'csv' +require 'json' + +def gather_sensors + result = {} + SenseThing.discover_devices do |dev| + dev.each_sensor do |sensor| + result[sensor.name] = sensor + end + end + result +end + +def gather_relevant_sensors(names) + sensors = gather_sensors + names.map do |name| + s = sensors[name] + raise "No sensor with name '#{name}'" unless s + + s + end +end + +def putline + puts '=' * 80 +end + +def list_sensors + SenseThing.discover_devices do |dev| + dev.each_sensor do |sensor| + puts "#{sensor.name.to_s.ljust(29)} #{sensor.summary}" + end + end +end + +def sensor_info(names) + names ||= [] + sensors = gather_relevant_sensors(names) + + sensors.each_with_index do |sensor, i| + puts "#{sensor.name.to_s.ljust(29)} #{sensor.summary}" + puts sensor.detail + putline if i < sensors.length - 1 + end +end + +def read_sensors(names) + names ||= [] + sensors = gather_relevant_sensors(names) + + sensors.each do |sensor| + # p sensor + puts "#{sensor.name.to_s.ljust(29)} #{sensor.fetch} #{sensor.unit}" + end +end + +def log_csv(sensors, interval, units) + $stdout.sync = true + CSV($stdout.dup) do |csv| + csv << sensors.map do |s| + if units + "#{s.name} [#{s.unit}]" + else + s.name.to_s + end + end + loop do + csv << sensors.map(&:fetch) + sleep interval + end + end +end + +def log_json(sensors, interval, units) + $stdout.sync = true + loop do + data = sensors.map do |s| + value = s.fetch + if units + [s.name.to_s, { value: value, unit: s.unit }] + else + [s.name.to_s, value] + end + end + data = data.to_h + JSON.dump(data, $stdout) + $stdout.write "\n" + sleep interval + end +end + +def log_sensors(names, format, interval, units) + names ||= [] + format ||= 'csv' + interval ||= 1 + sensors = gather_relevant_sensors(names) + + case format + when 'csv' + log_csv(sensors, interval, units) + when 'json' + log_json(sensors, interval, units) + else + raise "Invalid format: #{format}" + end +end + +args = SenseThing::Cli.parse_command_line + +case args&.cmd&.name +when nil + SenseThing::Cli.show_help + exit 0 +when 'list-sensors' + list_sensors +when 'info' + sensor_info(args.cmd[:name]) +when 'read' + read_sensors(args.cmd[:name]) +when 'log' + log_sensors(args.cmd[:name], args.cmd[:format], args.cmd[:interval], args.cmd[:units]) +end diff --git a/lib/sensething.rb b/lib/sensething.rb new file mode 100644 index 0000000..3106389 --- /dev/null +++ b/lib/sensething.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative 'sensething/sysfs' +require_relative 'sensething/nvidia' +require_relative 'sensething/cli' + +module SenseThing + def self.discover_devices(&block) + Dir.glob('/sys/class/hwmon/*').each do |path| + dev = begin + Sysfs::Hwmon.new(path) + rescue StandardError => e + warn "Tried to access a device at #{path.inspect}, but threw an exception: #{e}" + warn e.backtrace + end + + yield dev + end + + Dir.glob('/sys/devices/system/cpu/*/cpufreq') do |path| + dev = begin + Sysfs::Cpufreq.new(path) + rescue StandardError => e + warn "Tried to access a device at #{path.inspect}, but threw an exception: #{e}" + warn e.backtrace + end + + yield dev + end + + NvidiaSmi::SmiDevice.enumerate_gpus(&block) + end +end diff --git a/lib/sensething/cli.rb b/lib/sensething/cli.rb new file mode 100644 index 0000000..776d0c7 --- /dev/null +++ b/lib/sensething/cli.rb @@ -0,0 +1,141 @@ +require 'optparse' + +module SenseThing + VERSION = Gem.loaded_specs['sensething']&.version + + module Cli + class Command < OptionParser + attr_accessor :name, :short, :description + attr_reader :cmd + + def initialize(name, &blk) + super(&blk) + @name = name + @flag_vals = {} + on('-h', '--help', 'Show help message and exit') do + full_help_message + end + end + + def command(name, &block) + @commands ||= {} + opt = Command.new(name, &block) + banner_lead = banner.split(' - ', 2)[0] + opt.banner = "#{banner_lead} #{opt.name}" + opt.banner << " - #{opt.description}" if opt.description + @commands[opt.name.to_s] = opt + @commands[opt.short.to_s] = opt if opt.short + end + + def order!(argv = ARGV, into: nil, &nonopt) + @cmd = match_cmd(argv) + if @cmd + argv.shift + @cmd.order!(argv, into: into, &nonopt) + else + super(argv, into: into, &nonopt) + end + end + + def full_help_message + puts banner + if @commands + puts "\nCommands:" + cmds = Array(Set.new(@commands.values)).sort_by(&:name) + cmds.each do |c| + puts c.cmd_help + end + end + puts "\nOptions:" + puts summarize + exit 0 + end + + def [](k) + @flag_vals[k] + end + + def []=(k, v) + @flag_vals[k] = v + end + + protected + + def cmd_help + result = " #{name.rjust(12)} " + result << " #{short} " if short + result = result.ljust(26) + width = 80 - result.length + desc = description.scan(/\S.{0,#{width}}\S(?=\s|$)|\S+/) + result << desc.shift + desc.each do |line| + result << "\n" + result << line.rjust(80) + end + result + end + + private + + def match_cmd(argv) + return unless @commands + + @commands[argv[0]] + end + end + + @option_parser = Command.new('sensething') do |parser| # rubocop:disable Metrics/BlockLength + parser.banner = 'sensething' + parser.command 'list-sensors' do |ls| + ls.description = 'List all available sensors' + ls.short = 'ls' + end + parser.command 'info' do |si| + si.description = 'Show detailed information about sensors' + si.short = 'i' + si.on '-s NAME', '--sensor NAME', 'Name of a sensor to show information about' do |n| + si[:name] = [] unless si[:name] + si[:name] << n + end + end + parser.command 'read' do |r| + r.description = 'Read sensor values' + r.short = 'r' + r.on '-s NAME', '--sensor NAME', 'Name of a sensor to read' do |n| + r[:name] = [] unless r[:name] + r[:name] << n + end + end + parser.command 'log' do |l| + l.description = 'Log sensor values continuously' + l.short = 'l' + l.on '-s NAME', '--sensor NAME', 'Name of a sensor to include in the logs' do |n| + l[:name] = [] unless l[:name] + l[:name] << n + end + l.on '-f FORMAT', '--format FORMAT', 'Output data format' do |f| + l[:format] = f + end + l.on '-i SECONDS', '--interval SECONDS', 'Data logging interval in seconds' do |i| + l[:interval] = Float(i) + end + l.on '-u', '--units', 'Include units in output data' do |_| + l[:units] = true + end + end + parser.on '-v', '--version', 'Show version info and exit' do + puts "Sensething #{VERSION}" + exit 0 + end + end + + def self.parse_command_line(argv = ARGV) + @option_parser.order!(argv) + @option_parser + end + + def self.show_help + @option_parser.full_help_message + end + end +end diff --git a/lib/sensething/common/attribute.rb b/lib/sensething/common/attribute.rb new file mode 100644 index 0000000..e62ac5b --- /dev/null +++ b/lib/sensething/common/attribute.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module SenseThing + class Attribute + def fetch + @val = read + end + + def value(fetch: false) + return @val unless fetch || @val.nil? + + self.fetch + end + + def unit + nil + end + + module DecimalNumber + def fetch + @val = Float(read) + end + end + + module IntegralNumber + def fetch + @val = Integer(read) + end + end + end +end diff --git a/lib/sensething/common/device.rb b/lib/sensething/common/device.rb new file mode 100644 index 0000000..9891a8d --- /dev/null +++ b/lib/sensething/common/device.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module SenseThing + class Device + def each_attribute(&block) + return @attrs if @attrs + + discover_attributes.each(&block) + end + + def each_sensor + raise 'TODO' + end + + def name + raise 'TODO' + end + end +end diff --git a/lib/sensething/nvidia.rb b/lib/sensething/nvidia.rb new file mode 100644 index 0000000..f5b8e18 --- /dev/null +++ b/lib/sensething/nvidia.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative 'nvidia/smi' diff --git a/lib/sensething/nvidia/attribute.rb b/lib/sensething/nvidia/attribute.rb new file mode 100644 index 0000000..02f54e3 --- /dev/null +++ b/lib/sensething/nvidia/attribute.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative '../common/attribute' + +module Nvidia + class Attribute < SenseThing::Attribute + end +end diff --git a/lib/sensething/nvidia/device.rb b/lib/sensething/nvidia/device.rb new file mode 100644 index 0000000..e517a14 --- /dev/null +++ b/lib/sensething/nvidia/device.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative '../common/device' + +module Nvidia + class Device < SenseThing::Device + attr_reader :name, :uuid + + def initialize(name, uuid) + super() + @name = name + @uuid = uuid + end + end +end diff --git a/lib/sensething/nvidia/ffi.rb b/lib/sensething/nvidia/ffi.rb new file mode 100644 index 0000000..3601aac --- /dev/null +++ b/lib/sensething/nvidia/ffi.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'mkmf' +require 'fiddle' + +if have_library('nvidia-ml') + module Nvidia + module Ffi + extend Fiddle::Importer + dlload 'libnvidia-ml.so' + end + end +end + +if $nvml_dll + module Nvidia + module Ffi + # The operation was successful. + NVML_SUCCESS = 0 + # NVML was not first initialized with nvmlInit(). + NVML_ERROR_UNINITIALIZED = 1 + # A supplied argument is invalid. + NVML_ERROR_INVALID_ARGUMENT = 2 + # The requested operation is not available on target device. + NVML_ERROR_NOT_SUPPORTED = 3 + # The current user does not have permission for operation. + NVML_ERROR_NO_PERMISSION = 4 + # Deprecated: Multiple initializations are now allowed through ref counting. + NVML_ERROR_ALREADY_INITIALIZED = 5 + # A query to find an object was unsuccessful. + NVML_ERROR_NOT_FOUND = 6 + # An input argument is not large enough. + NVML_ERROR_INSUFFICIENT_SIZE = 7 + # A device's external power cables are not properly attached. + NVML_ERROR_INSUFFICIENT_POWER = 8 + # NVIDIA driver is not loaded. + NVML_ERROR_DRIVER_NOT_LOADED = 9 + # User provided timeout passed. + NVML_ERROR_TIMEOUT = 10 + # NVIDIA Kernel detected an interrupt issue with a GPU. + NVML_ERROR_IRQ_ISSUE = 11 + # NVML Shared Library couldn't be found or loaded. + NVML_ERROR_LIBRARY_NOT_FOUND = 12 + # Local version of NVML doesn't implement this function. + NVML_ERROR_FUNCTION_NOT_FOUND = 13 + # infoROM is corrupted + NVML_ERROR_CORRUPTED_INFOROM = 14 + # The GPU has fallen off the bus or has otherwise become inaccessible. + NVML_ERROR_GPU_IS_LOST = 15 + # The GPU requires a reset before it can be used again. + NVML_ERROR_RESET_REQUIRED = 16 + # The GPU control device has been blocked by the operating system/cgroups. + NVML_ERROR_OPERATING_SYSTEM = 17 + # RM detects a driver/library version mismatch. + NVML_ERROR_LIB_RM_VERSION_MISMATCH = 18 + # An operation cannot be performed because the GPU is currently in use. + NVML_ERROR_IN_USE = 19 + # Insufficient memory. + NVML_ERROR_MEMORY = 20 + # No data. + NVML_ERROR_NO_DATA = 21 + # The requested vgpu operation is not available on target device, becasue ECC is enabled. + NVML_ERROR_VGPU_ECC_NOT_SUPPORTED = 22 + # Ran out of critical resources, other than memory. + NVML_ERROR_INSUFFICIENT_RESOURCES = 23 + # Ran out of critical resources, other than memory. + NVML_ERROR_FREQ_NOT_SUPPORTED = 24 + # The provided version is invalid/unsupported. + NVML_ERROR_ARGUMENT_VERSION_MISMATCH = 25 + # The requested functionality has been deprecated. + NVML_ERROR_DEPRECATED = 26 + # The system is not ready for the request. + NVML_ERROR_NOT_READY = 27 + # An internal driver error occurred. + NVML_ERROR_UNKNOWN = 999 + + NVML_SYSTEM_NVML_VERSION_BUFFER_SIZE = 80 + NVML_DEVICE_NAME_V2_BUFFER_SIZE = 96 + NVML_DEVICE_SERIAL_BUFFER_SIZE = 30 + NVML_DEVICE_UUID_V2_BUFFER_SIZE = 96 + + Fiddle.struct + + INIT_WITH_FLAGS = Fiddle::Function.new( + $nvml_dll['nvmlInitWithFlags'], + [Fiddle::TYPE_UINT], + Fiddle::TYPE_INT + ) + + SHUT_DOWN = Fiddle::Function.new( + $nvml_dll['nvmlShutdown'], + [], + Fiddle::TYPE_INT + ) + + SYSTEM_GET_NVML_VERSION = Fiddle::Function.new( + $nvml_dll['nvmlSystemGetNVMLVersion'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT], + Fiddle::TYPE_INT + ) + + ERROR_STRING = Fiddle::Function.new( + $nvml_dll['nvmlErrorString'], + [Fiddle::TYPE_INT], + Fiddle::TYPE_VOIDP + ) + + GET_DEVICE_COUNT_V2 = Fiddle::Function.new( + $nvml_dll['nvmlDeviceGetCount_v2'], + [Fiddle::TYPE_UINTPTR_T], + Fiddle::TYPE_INT + ) + + GET_DEVICE_HANDLE_BY_INDEX_V2 = Fiddle::Function.new( + $nvml_dll['nvmlDeviceGetHandleByIndex_v2'], + [Fiddle::TYPE_UINT, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_INT + ) + end + end + + if Nvidia::Ffi::INIT_WITH_FLAGS.call(1) == Nvidia::Ffi::NVML_SUCCESS + at_exit do + Nvidia::Ffi::SHUT_DOWN.call + end + else + $nvml_dll = nil + end +end + +module Nvidia + def self.has_nvml? + !$nvml_dll.nil? + end +end diff --git a/lib/sensething/nvidia/smi.rb b/lib/sensething/nvidia/smi.rb new file mode 100644 index 0000000..c75410c --- /dev/null +++ b/lib/sensething/nvidia/smi.rb @@ -0,0 +1,489 @@ +# frozen_string_literal: true + +require_relative 'device' +require_relative 'attribute' +require 'open3' +require 'csv' + +module NvidiaSmi + class SmiAttribute < Nvidia::Attribute + attr_accessor :type + + def initialize(key, type, fetch_fn) + super() + @key = key.strip + @type = type + @fetch_fn = fetch_fn + end + + def read + @fetch_fn.call[@key][0] + end + + module Celsius + def unit + '°C' + end + end + + module Rpm + include Nvidia::Attribute::DecimalNumber + def unit + 'RPM' + end + end + + module Megahertz + def fetch + raw = read + raise "Invalid nvidia-smi frequency string: #{raw.inspect}" unless /([0-9.]+) +MHz/ =~ raw + + @val = Float($1) + end + + def unit + 'MHz' + end + end + + module Watts + def fetch + raw = read + raise "Invalid nvidia-smi power string: #{raw.inspect}" unless /([0-9.]+) +W/ =~ raw + + @val = Float($1) + end + + def unit + 'W' + end + end + + module Percentage + def fetch + raw = read + raise "Invalid nvidia-smi percentage string: #{raw.inspect}" unless /([0-9.]+) +%/ =~ raw + + @val = Float($1) / 100.0 + end + end + end + + class SmiDevice < Nvidia::Device + attr_reader :index + + def initialize(name, uuid, index) + super(name, uuid) + @index = Integer(index) + end + + def self.enumerate_gpus + Open3.popen3('nvidia-smi', '-L') do |i, o, _e, _t| + i.close + o.read.each_line do |line| + line.strip! + if /^GPU ([0-9]+): ([a-zA-Z0-9\- ]+) \(UUID: ([a-zA-Z0-9-]+)\)/ =~ line + yield SmiDevice.new($2, $3, $1) + end + end + end + end + + def each_sensor # rubocop:disable Metrics/CyclomaticComplexity + remain = Array(each_attribute) + until remain.empty? + a0 = remain.shift + sens_attrs, remain = remain.partition { |a| a0.type == a.type } + sens_attrs << a0 + + case a0.type + when :temperature + yield NvidiaSmiTemperatureSensor.new(self, sens_attrs) + when :shader_clock + yield NvidiaSmiShaderFrequencySensor.new(self, sens_attrs) + when :mem_clock + yield NvidiaSmiMemoryFrequencySensor.new(self, sens_attrs) + when :video_clock + yield NvidiaSmiVideoFrequencySensor.new(self, sens_attrs) + when :pcie_gen + yield NvidiaSmiPcieLinkGenSensor.new(self, sens_attrs) + when :pcie_width + yield NvidiaSmiPcieLinkWidthSensor.new(self, sens_attrs) + when :power + yield NvidiaSmiPowerSensor.new(self, sens_attrs) + when :fan + yield NvidiaSmiFanSensor.new(self, sens_attrs) + end + end + end + + private + + def query_smi + Open3.popen3('nvidia-smi', "--id=#{@uuid}", "--query-gpu=#{@attr_keys.join(',')}", + '--format=csv') do |i, o, _e, t| + i.close + return CSV.parse(o.read.strip, headers: true, strip: true) if t.value.success? + end + nil + end + + def update_cached_smi_response + now = DateTime.now + return @cached_smi_response unless @last_update.nil? || (now - @last_update >= Rational(1, 86_401)) + + @last_update = now + @cached_smi_response = query_smi + end + + protected + + def discover_attributes # rubocop:disable Metrics/MethodLength + attrs = [] + @attr_keys = [] + fetch_fn = -> { update_cached_smi_response } + # Probe attributes individually to figure out which ones are available + discover_attribute('temperature.gpu') do |k| + attrs << TemperatureValue.new('temperature.gpu', :temperature, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.current.graphics') do |k| + attrs << ShaderFrequencyValue.new('clocks.current.graphics [MHz]', :shader_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.max.graphics') do |k| + attrs << ShaderFrequencyMax.new('clocks.max.graphics [MHz]', :shader_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.current.memory') do |k| + attrs << MemoryFrequencyValue.new('clocks.current.memory [MHz]', :mem_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.max.memory') do |k| + attrs << MemoryFrequencyMax.new('clocks.max.memory [MHz]', :mem_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.current.video') do |k| + attrs << VideoFrequencyValue.new('clocks.current.video [MHz]', :video_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('clocks.max.video') do |k| + attrs << VideoFrequencyMax.new('clocks.max.video [MHz]', :video_clock, fetch_fn) + @attr_keys << k + end + discover_attribute('pcie.link.gen.gpucurrent') do |k| + attrs << PcieLinkGenValue.new('pcie.link.gen.gpucurrent', :pcie_gen, fetch_fn) + @attr_keys << k + end + discover_attribute('pcie.link.gen.max') do |k| + attrs << PcieLinkGenMax.new('pcie.link.gen.max', :pcie_gen, fetch_fn) + @attr_keys << k + end + discover_attribute('pcie.link.gen.gpumax') do |k| + attrs << PcieLinkGenSupportedByGpu.new('pcie.link.gen.gpumax', :pcie_gen, fetch_fn) + @attr_keys << k + end + discover_attribute('pcie.link.width.current') do |k| + attrs << PcieLinkWidthValue.new('pcie.link.width.current', :pcie_width, fetch_fn) + @attr_keys << k + end + discover_attribute('pcie.link.width.max') do |k| + attrs << PcieLinkWidthMax.new('pcie.link.width.max', :pcie_width, fetch_fn) + @attr_keys << k + end + discover_attribute('power.draw') do |k| + attrs << PowerValue.new('power.draw [W]', :power, fetch_fn) + @attr_keys << k + end + discover_attribute('power.limit') do |k| + attrs << PowerMax.new('power.limit [W]', :power, fetch_fn) + @attr_keys << k + end + discover_attribute('fan.speed') do |k| + attrs << FanSpeedValue.new('fan.speed [%]', :fan, fetch_fn) + @attr_keys << k + end + @attrs = attrs + end + + private + + def discover_attribute(key) + Open3.popen3('nvidia-smi', "--id=#{uuid}", "--query-gpu=#{key}", '--format=csv,noheader') do |i, _o, _e, t| + i.close + yield key if t.value.success? + end + end + end + + class SmiSensor < SenseThing::Sensor + attr_reader :device, :value_attr, :min_attr, :max_attr + + def initialize(device) + super() + @device = device + end + + def unit + value_attr&.unit + end + + def fetch + value_attr&.fetch + end + + def value(fetch: false) + value_attr&.value(fetch: fetch) + end + + def minimum + min_attr&.value + end + + def maximum + max_attr&.value + end + + def units + value_attr&.unit + end + end + + class NvidiaSmiTemperatureSensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when TemperatureValue + @value_attr = a + end + end + end + + def name + "nvidia#{device.index}/temperature" + end + + def summary + "Temperature Sensor (nvidia#{device.index})" + end + + def detail + result = [] + result << "gpu: #{device.name}" + result << "gpuid: #{device.uuid}" + end + end + + class NvidiaSmiShaderFrequencySensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when ShaderFrequencyValue + @value_attr = a + when ShaderFrequencyMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/shader_frequency" + end + + def summary + "Shader Frequency (nvidia#{device.index})" + end + end + + class NvidiaSmiMemoryFrequencySensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when MemoryFrequencyValue + @value_attr = a + when MemoryFrequencyMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/mem_frequency" + end + + def summary + "Memory Frequency (nvidia#{device.index})" + end + end + + class NvidiaSmiVideoFrequencySensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when VideoFrequencyValue + @value_attr = a + when VideoFrequencyMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/video_frequency" + end + + def summary + "Video Frequency (nvidia#{device.index})" + end + end + + class NvidiaSmiPcieLinkGenSensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when PcieLinkGenValue + @value_attr = a + when PcieLinkGenMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/pcie_gen" + end + + def summary + "PCIe Link Generation (nvidia#{device.index})" + end + end + + class NvidiaSmiPcieLinkWidthSensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when PcieLinkWidthValue + @value_attr = a + when PcieLinkWidthMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/pcie_width" + end + + def summary + "PCIe Link Width (nvidia#{device.index})" + end + end + + class NvidiaSmiPowerSensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when PowerValue + @value_attr = a + when PowerMax + @max_attr = a + end + end + end + + def name + "nvidia#{device.index}/power" + end + + def summary + "Power Sensor (nvidia#{device.index})" + end + end + + class NvidiaSmiFanSensor < SmiSensor + def initialize(device, attrs) + super(device) + attrs.each do |a| + case a + when FanSpeedValue + @value_attr = a + end + end + end + + def name + "nvidia#{device.index}/fan" + end + + def summary + "Fan (nvidia#{device.index})" + end + end + + class TemperatureValue < SmiAttribute + include Celsius + end + + class ShaderFrequencyValue < SmiAttribute + include Megahertz + end + + class ShaderFrequencyMax < SmiAttribute + include Megahertz + end + + class MemoryFrequencyValue < SmiAttribute + include Megahertz + end + + class MemoryFrequencyMax < SmiAttribute + include Megahertz + end + + class VideoFrequencyValue < SmiAttribute + include Megahertz + end + + class VideoFrequencyMax < SmiAttribute + include Megahertz + end + + class PcieLinkGenValue < SmiAttribute + include Nvidia::Attribute::IntegralNumber + end + + class PcieLinkGenMax < SmiAttribute + include Nvidia::Attribute::IntegralNumber + end + + class PcieLinkGenSupportedByGpu < SmiAttribute + include Nvidia::Attribute::IntegralNumber + end + + class PcieLinkWidthValue < SmiAttribute + include Nvidia::Attribute::IntegralNumber + end + + class PcieLinkWidthMax < SmiAttribute + include Nvidia::Attribute::IntegralNumber + end + + class PowerValue < SmiAttribute + include Watts + end + + class PowerMax < SmiAttribute + include Watts + end + + class FanSpeedValue < SmiAttribute + include Percentage + end +end diff --git a/lib/sensething/sensor.rb b/lib/sensething/sensor.rb new file mode 100644 index 0000000..fad07f5 --- /dev/null +++ b/lib/sensething/sensor.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module SenseThing + class Sensor + def name + raise 'TODO' + end + + def fetch + raise 'TODO' + end + + def value(fetch: false) + @value = self.fetch if fetch + @value + end + + def minimum; end + def maximum; end + + def limits + [minimum, maximum] + end + + def summary + raise 'TODO' + end + + def detail + raise 'TODO' + end + + def unit + raise 'TODO' + end + end +end diff --git a/lib/sensething/sysfs.rb b/lib/sensething/sysfs.rb new file mode 100644 index 0000000..ec132cb --- /dev/null +++ b/lib/sensething/sysfs.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative 'sysfs/hwmon' +require_relative 'sysfs/cpufreq' +require_relative 'sysfs/drm' diff --git a/lib/sensething/sysfs/attribute.rb b/lib/sensething/sysfs/attribute.rb new file mode 100644 index 0000000..838a211 --- /dev/null +++ b/lib/sensething/sysfs/attribute.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative '../common/attribute' +require 'pathname' + +module Sysfs + class Attribute < SenseThing::Attribute + attr_reader :path + + def initialize(path) + super() + @path = path + @path = Pathname.new(@path) unless @path.instance_of? Pathname + @path = @path.realpath + return if @path.file? + + raise "Not a file: #{@path.inspect}" + end + + def read + File.read(path) + end + + def same_sensor?(_other) + raise 'TODO' + end + end +end diff --git a/lib/sensething/sysfs/cpufreq.rb b/lib/sensething/sysfs/cpufreq.rb new file mode 100644 index 0000000..d15b233 --- /dev/null +++ b/lib/sensething/sysfs/cpufreq.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'pathname' +require_relative 'attribute' +require_relative 'device' + +module Sysfs + class CpufreqAttribute < Attribute + module Megahertz + def fetch + # hwmon measures in kilohertz + @val = Float(read) / 1000.0 + end + + def unit + 'MHz' + end + + def same_sensor?(_other) + true # for cpufreq, each 'device' represents a single sensor + end + end + end + + class Cpufreq < Device + def self.parse_attr_by_name(name, path) + case name + when 'scaling_cur_freq' + FrequencyValue.new(path) + when 'scaling_min_freq' + FrequencyMin.new(path) + when 'scaling_max_freq' + FrequencyMax.new(path) + when 'scaling_governor' + Governor.new(path) + end + end + + def create_sensor(attrs) + CpuFreqSensor.new(self, attrs) + end + + protected + + def discover_attributes + attrs = [] + @path.each_child do |pn| + if (attr = self.class.parse_attr_by_name(pn.basename.to_s, pn)) + attrs << attr + end + end + @attrs = attrs + end + + def discover_name + related_cpus = File.read(@path.join('related_cpus')).strip + @name = "cpu#{related_cpus.split(' ').join('_')}" + rescue Errno::ENOENT + @noname = true + end + end + + class CpuFreqSensor < SenseThing::Sensor + attr_reader :device, :min_attr, :max_attr, :value_attr, :governor_attr + + def initialize(device, attrs) + super() + @device = device + attrs.each do |a| + case a + when FrequencyMin + @min_attr = a + when FrequencyMax + @max_attr = a + when FrequencyValue + @value_attr = a + when Governor + @governor_attr = a + end + end + end + + def unit + value_attr&.unit + end + + def fetch + value_attr&.fetch + end + + def value(fetch: false) + value_attr&.value(fetch: fetch) + end + + def minimum + min_attr&.value + end + + def maximum + max_attr&.value + end + + def units + value_attr&.unit + end + + def name + "#{device.name}/frequency" + end + + def summary + "CPU Frequency (cpufreq/#{device.path.basename})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result << "governor: #{governor_attr.path}" if governor_attr + result << "min: #{min_attr.path}" if min_attr + result << "max: #{max_attr.path}" if max_attr + result.join("\n") + end + end + + class FrequencyValue < CpufreqAttribute + include Megahertz + end + + class FrequencyMin < CpufreqAttribute + include Megahertz + end + + class FrequencyMax < CpufreqAttribute + include Megahertz + end + + class Governor < CpufreqAttribute + def fetch + @val = read.strip + end + end +end diff --git a/lib/sensething/sysfs/device.rb b/lib/sensething/sysfs/device.rb new file mode 100644 index 0000000..a478101 --- /dev/null +++ b/lib/sensething/sysfs/device.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require_relative '../common/device' +require 'pathname' + +module Sysfs + class Device < SenseThing::Device + attr_reader :path + + def initialize(path) + super() + @path = path + @path = Pathname.new(@path) unless @path.instance_of? Pathname + @path = @path.realpath + raise "Not a directory: #{@path.inspect}" unless @path.directory? + + @attrs = nil + @name = nil + @noname = false + end + + def name + return @name if @name || @noname + + discover_name + end + + def each_attribute(&block) + return @attrs if @attrs + + discover_attributes.each(&block) + end + + def each_sensor + remain = Array(each_attribute) + until remain.empty? + a0 = remain.shift + sens_attrs, remain = remain.partition { |a| a0.same_sensor? a } + sens_attrs << a0 + yield create_sensor(sens_attrs) + end + end + + def discover_attributes + raise 'TODO' + end + + def create_sensor(_attrs) + raise 'TODO' + end + end +end diff --git a/lib/sensething/sysfs/drm.rb b/lib/sensething/sysfs/drm.rb new file mode 100644 index 0000000..33494b7 --- /dev/null +++ b/lib/sensething/sysfs/drm.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'pathname' +require_relative 'attribute' +require_relative 'device' + +# TODO +module Sysfs +end diff --git a/lib/sensething/sysfs/hwmon.rb b/lib/sensething/sysfs/hwmon.rb new file mode 100644 index 0000000..cc69377 --- /dev/null +++ b/lib/sensething/sysfs/hwmon.rb @@ -0,0 +1,702 @@ +# frozen_string_literal: true + +require 'pathname' +require_relative '../sensor' +require_relative 'attribute' +require_relative 'device' + +module Sysfs + class HwmonAttribute < Attribute + attr_reader :chan_num + + def initialize(path, chan_num) + super(path) + @chan_num = Integer(chan_num) + end + + def type + raise 'TODO' + end + + def same_sensor?(other) + chan_num == other.chan_num && type == other.type + end + + module Millivolts + include Attribute::DecimalNumber + def unit + 'mV' + end + + def type + :voltage + end + end + + module Milliamps + include Attribute::DecimalNumber + def unit + 'mA' + end + + def type + :current + end + end + + module Rpm + include Attribute::DecimalNumber + def unit + 'RPM' + end + + def type + :fan + end + end + + module Celsius + def fetch + # hwmon measures in millidegrees + @val = Float(read) / 1000.0 + end + + def unit + '°C' + end + + def type + :temperature + end + end + end + + class Hwmon < Device + def self.parse_attr_by_name(name, path) + if name.start_with? 'in' + chan_num, type = parse_attr_num_text(name[2..]) + parse_in_attr(chan_num, type, path) + elsif name.start_with? 'curr' + chan_num, type = parse_attr_num_text(name[4..]) + parse_curr_attr(chan_num, type, path) + elsif name.start_with? 'fan' + chan_num, type = parse_attr_num_text(name[3..]) + parse_fan_attr(chan_num, type, path) + elsif name.start_with? 'pwm' + chan_num, type = parse_attr_num_text(name[3..]) + parse_pwm_attr(chan_num, type, path) + elsif name.start_with? 'temp' + chan_num, type = parse_attr_num_text(name[4..]) + parse_temperature_attr(chan_num, type, path) + end + end + + def create_sensor(attrs) + case attrs[0].type + when :voltage + HwmonVoltageSensor.new(self, attrs[0].type, attrs[0].chan_num, attrs) + when :current + HwmonCurrentSensor.new(self, attrs[0].type, attrs[0].chan_num, attrs) + when :temperature + HwmonTemperatureSensor.new(self, attrs[0].type, attrs[0].chan_num, attrs) + when :fan + HwmonFanSensor.new(self, attrs[0].type, attrs[0].chan_num, attrs) + when :pwm + HwmonPwmSensor.new(self, attrs[0].type, attrs[0].chan_num, attrs) + end + end + + protected + + def discover_attributes + attrs = [] + @path.each_child do |pn| + if (attr = self.class.parse_attr_by_name(pn.basename.to_s, pn)) + attrs << attr + end + end + @attrs = attrs.sort_by { |a| [a.chan_num, a.path.basename.to_s] } + end + + def discover_name + @name = File.read(@path.join('name')).strip + rescue Errno::ENOENT + @noname = true + end + + class << self + def parse_in_attr(chan_num, type, path) + case type + when 'min' + VoltageMin.new(path, chan_num) + when 'lcrit' + VoltageCriticalMin.new(path, chan_num) + when 'max' + VoltageMax.new(path, chan_num) + when 'crit' + VoltageCriticalMax.new(path, chan_num) + when 'input' + VoltageValue.new(path, chan_num) + when 'average' + VoltageAverage.new(path, chan_num) + when 'lowest' + VoltageLowest.new(path, chan_num) + when 'highest' + VoltageHighest.new(path, chan_num) + when 'label' + VoltageLabel.new(path, chan_num) + end + end + + def parse_curr_attr(chan_num, type, path) + case type + when 'min' + CurrentMin.new(path, chan_num) + when 'lcrit' + CurrentCriticalMin.new(path, chan_num) + when 'max' + CurrentMax.new(path, chan_num) + when 'crit' + CurrentCriticalMax.new(path, chan_num) + when 'input' + CurrentValue.new(path, chan_num) + when 'average' + CurrentAverage.new(path, chan_num) + when 'lowest' + CurrentLowest.new(path, chan_num) + when 'highest' + CurrentHighest.new(path, chan_num) + end + end + + def parse_fan_attr(chan_num, type, path) + case type + when 'min' + FanMin.new(path, chan_num) + when 'max' + FanMax.new(path, chan_num) + when 'input' + FanValue.new(path, chan_num) + when 'div' + FanDivisor.new(path, chan_num) + when 'pulses' + FanPulses.new(path, chan_num) + when 'target' + FanTarget.new(path, chan_num) + when 'label' + FanLabel.new(path, chan_num) + when 'enable' + FanEnabled.new(path, chan_num) + end + end + + def parse_pwm_attr(chan_num, type, path) + case type + when nil + PwmValue.new(path, chan_num) if chan_num + when 'enable' + PwmEnabled.new(path, chan_num) + end + end + + def parse_temperature_attr(chan_num, type, path) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity + case type + when 'type' + TemperatureType.new(path, chan_num) + when 'max' + TemperatureMax.new(path, chan_num) + when 'min' + TemperatureMin.new(path, chan_num) + when 'max_hyst' + TemperatureMaxHysteresis.new(path, chan_num) + when 'min_hyst' + TemperatureMinHysteresis.new(path, chan_num) + when 'input' + TemperatureValue.new(path, chan_num) + when 'crit' + TemperatureCriticalMax.new(path, chan_num) + when 'crit_hyst' + TemperatureCriticalMaxHysteresis.new(path, chan_num) + when 'lcrit' + TemperatureCriticalMin.new(path, chan_num) + when 'lcrit_hyst' + TemperatureCriticalMinHysteresis.new(path, chan_num) + when 'emergency' + TemperatureEmergency.new(path, chan_num) + when 'emergency_hyst' + TemperatureEmergencyHysteresis.new(path, chan_num) + when 'lowest' + TemperatureLowest.new(path, chan_num) + when 'highest' + TemperatureHighest.new(path, chan_num) + when 'label' + TemperatureLabel.new(path, chan_num) + when 'enable' + TemperatureEnable.new(path, chan_num) + end + end + + def parse_attr_num_text(subname) + return unless /^([0-9]+)/ =~ subname + + num = $1 + return [num] if num.length == subname.length + + subname = subname[num.length..] + + return unless /^_([a-z_]+)$/ =~ subname + + [num, $1] + end + end + end + + class HwmonSensor < SenseThing::Sensor + attr_reader :device, :label_attr, :value_attr, :min_attr, :max_attr + + def initialize(device, type, chan_num) + super() + @device = device + @type = type + @chan_num = chan_num + end + + def name + if label_attr + "#{device.name}/#{label_attr.value}" + else + "#{device.name}/#{@type}_#{@chan_num}" + end + end + + def unit + value_attr&.unit + end + + def fetch + value_attr&.fetch + end + + def value(fetch: false) + value_attr&.value(fetch: fetch) + end + + def minimum + min_attr&.value + end + + def maximum + max_attr&.value + end + + def units + value_attr&.unit + end + end + + class HwmonVoltageSensor < HwmonSensor + def initialize(device, type, chan_num, attrs) + super(device, type, chan_num) + attrs.each do |a| + case a + when VoltageMin + @min_attr = a + when VoltageMax + @max_attr = a + when VoltageCriticalMin + @crit_min_attr = a + when VoltageCriticalMax + @crit_max_attr = a + when VoltageValue + @value_attr = a + when VoltageLabel + @label_attr = a + end + end + end + + def summary + "Voltage Sensor (#{device.path.basename}/in#{@chan_num})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result << "label: #{label_attr.path}" if label_attr + result << "min: #{min_attr.path}" if min_attr + result << "max: #{max_attr.path}" if max_attr + result << "critical min: #{crit_min_attr.path}" if @crit_min_attr + result << "critical max: #{crit_max_attr.path}" if @crit_max_attr + result.join("\n") + end + end + + class HwmonCurrentSensor < HwmonSensor + def initialize(device, type, chan_num, attrs) + super(device, type, chan_num) + attrs.each do |a| + case a + when CurrentMin + @min_attr = a + when CurrentMax + @max_attr = a + when CurrentCriticalMin + @crit_min_attr = a + when CurrentCriticalMax + @crit_max_attr = a + when CurrentValue + @value_attr = a + when CurrentLabel + @label_attr = a + end + end + end + + def summary + "Current Sensor (#{device.path.basename}/curr#{@chan_num})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result << "label: #{label_attr.path}" if label_attr + result << "min: #{min_attr.path}" if min_attr + result << "max: #{max_attr.path}" if max_attr + result << "critical min: #{crit_min_attr.path}" if @crit_min_attr + result << "critical max: #{crit_max_attr.path}" if @crit_max_attr + result.join("\n") + end + end + + class HwmonTemperatureSensor < HwmonSensor + def initialize(device, type, chan_num, attrs) + super(device, type, chan_num) + attrs.each do |a| + case a + when TemperatureMin + @min_attr = a + when TemperatureMax + @max_attr = a + when TemperatureValue + @value_attr = a + when TemperatureLabel + @label_attr = a + when TemperatureType + @temp_type_attr = a + end + end + end + + def summary + "Temperature Sensor (#{device.path.basename}/temp#{@chan_num})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result << "label: #{label_attr.path}" if label_attr + result << "min: #{min_attr.path}" if min_attr + result << "max: #{max_attr.path}" if max_attr + result << "type: #{@temp_type_attr.path}" if @temp_type_attr + result.join("\n") + end + end + + class HwmonFanSensor < HwmonSensor + def initialize(device, type, chan_num, attrs) + super(device, type, chan_num) + attrs.each do |a| + case a + when FanMin + @min_attr = a + when FanMax + @max_attr = a + when FanValue + @value_attr = a + when FanLabel + @label_attr = a + end + end + end + + def summary + "Fan (#{device.path.basename}/fan#{@chan_num})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result << "label: #{label_attr.path}" if label_attr + result << "min: #{min_attr.path}" if min_attr + result << "max: #{max_attr.path}" if max_attr + result.join("\n") + end + end + + class HwmonPwmSensor < HwmonSensor + def initialize(device, type, chan_num, attrs) + super(device, type, chan_num) + attrs.each do |a| + case a + when PwmValue + @value_attr = a + end + end + end + + def summary + "PWM (#{device.path.basename}/pwm#{@chan_num})" + end + + def detail + result = [] + result << "value: #{value_attr.path}" if value_attr + result.join("\n") + end + end + + class VoltageMin < HwmonAttribute + include Millivolts + end + + class VoltageCriticalMin < HwmonAttribute + include Millivolts + end + + class VoltageMax < HwmonAttribute + include Millivolts + end + + class VoltageCriticalMax < HwmonAttribute + include Millivolts + end + + class VoltageValue < HwmonAttribute + include Millivolts + end + + class VoltageAverage < HwmonAttribute + include Millivolts + end + + class VoltageLowest < HwmonAttribute + include Millivolts + end + + class VoltageHighest < HwmonAttribute + include Millivolts + end + + class VoltageLabel < HwmonAttribute + def fetch + @val = read.strip.gsub(/\s+/, '_') + end + + def type + :voltage + end + end + + class CurrentMin < HwmonAttribute + include Milliamps + end + + class CurrentCriticalMin < HwmonAttribute + include Milliamps + end + + class CurrentMax < HwmonAttribute + include Milliamps + end + + class CurrentCriticalMax < HwmonAttribute + include Milliamps + end + + class CurrentValue < HwmonAttribute + include Milliamps + end + + class CurrentAverage < HwmonAttribute + include Milliamps + end + + class CurrentLowest < HwmonAttribute + include Milliamps + end + + class CurrentHighest < HwmonAttribute + include Milliamps + end + + class FanMin < HwmonAttribute + include Rpm + end + + class FanMax < HwmonAttribute + include Rpm + end + + class FanValue < HwmonAttribute + include Rpm + end + + class FanDivisor < HwmonAttribute + include IntegralNumber + def type + :fan + end + end + + class FanPulses < HwmonAttribute + include IntegralNumber + def type + :fan + end + end + + class FanTarget < HwmonAttribute + include Rpm + def type + :fan + end + end + + class FanLabel < HwmonAttribute + def fetch + @val = read.strip + end + + def type + :fan + end + end + + class FanEnabled < HwmonAttribute + def fetch + @val = read.strip != '0' + end + + def type + :fan + end + end + + class PwmValue < HwmonAttribute + def fetch + @val = Float(read) / 255.0 + end + + def type + :pwm + end + end + + class PwmEnabled < HwmonAttribute + def fetch + @val = read.strip != '0' + end + + def type + :pwm + end + end + + class TemperatureType < HwmonAttribute + CPU_DIODE = 1 + TRANSISTOR_3904 = 2 + THERMAL_DIODE = 3 + THERMISTOR = 4 + AMDSI = 5 + PECI = 6 + + @type_vals = { + '1': CPU_DIODE, + '2': TRANSISTOR_3904, + '3': THERMAL_DIODE, + '4': THERMISTOR, + '5': AMDSI, + '6': PECI + } + + def fetch + self.class.type_vals[read.strip] + end + + def type + :temperature + end + end + + class TemperatureMax < HwmonAttribute + include Celsius + end + + class TemperatureMin < HwmonAttribute + include Celsius + end + + class TemperatureMaxHysteresis < HwmonAttribute + include Celsius + end + + class TemperatureMinHysteresis < HwmonAttribute + include Celsius + end + + class TemperatureValue < HwmonAttribute + include Celsius + end + + class TemperatureCriticalMax < HwmonAttribute + include Celsius + end + + class TemperatureCriticalMaxHysteresis < HwmonAttribute + include Celsius + end + + class TemperatureCriticalMin < HwmonAttribute + include Celsius + end + + class TemperatureCriticalMinHysteresis < HwmonAttribute + include Celsius + end + + class TemperatureEmergency < HwmonAttribute + include Celsius + end + + class TemperatureEmergencyHysteresis < HwmonAttribute + include Celsius + end + + class TemperatureLowest < HwmonAttribute + include Celsius + end + + class TemperatureHighest < HwmonAttribute + include Celsius + end + + class TemperatureLabel < HwmonAttribute + def fetch + @val = read.strip + end + + def type + :temperature + end + end + + class TemperatureEnable < HwmonAttribute + def fetch + @val = read.strip != '0' + end + + def type + :temperature + end + end +end diff --git a/sensething.gemspec b/sensething.gemspec new file mode 100644 index 0000000..43899ac --- /dev/null +++ b/sensething.gemspec @@ -0,0 +1,14 @@ +Gem::Specification.new do |s| + s.name = 'sensething' + s.version = '0.0.1' + s.platform = Gem::Platform::RUBY + s.summary = 'Powerful sensor logging utility for Linux' + s.description = 'System-wide sensor data logging system with support for hwmon and nvidia-smi.' + s.authors = ['Evan Perry Grove'] + s.email = ['evan@4grove.com'] + s.homepage = 'https://hardfault.life' + s.license = 'GPL-3.0-only' + s.files = Dir.glob("{lib,bin}/**/*") # This includes all files under the lib directory recursively, so we don't have to add each one individually. + s.require_path = 'lib' + s.executables = ['sensething'] +end diff --git a/test/sanity_test.rb b/test/sanity_test.rb new file mode 100644 index 0000000..21c658f --- /dev/null +++ b/test/sanity_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require_relative '../lib/sensething' + +class SanityTest < Minitest::Test + def test_sanity + assert true + end +end