# -*- coding: UTF-8 -*- # This file is part of the jetson_stats package (https://github.com/rbonghi/jetson_stats or http://rnext.it). # Copyright (c) 2019-2023 Raffaello Bonghi. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from .common import cat, check_file import os # Logging import logging # Create logger logger = logging.getLogger(__name__) def total_power(power): """ Function to measure the total watt :return: Total power and a second dictionary with all other measures :rtype: dict, dict """ # In according with: # https://forums.developer.nvidia.com/t/power-consumption-monitoring/73608/8 # https://github.com/rbonghi/jetson_stats/issues/51 # https://forums.developer.nvidia.com/t/tegrastats-monitoring/217088/4?u=user62045 # https://forums.developer.nvidia.com/t/orin-nx-power-data-from-jtop/242804/5 total_name = "" for val in power: if val in ["POM_5V_IN", "VDD_IN"]: total_name = val break # Extract the total from list # Otherwise sum all values # Example for Jetson Xavier # https://forums.developer.nvidia.com/t/xavier-jetson-total-power-consumption/81016 if total_name: total = power[total_name] total['name'] = total_name del power[total_name] return total, power # Otherwise measure all total power total = {'power': 0, 'avg': 0} for value in power.values(): total['power'] += value['power'] total['avg'] += value['avg'] return total, power def find_driver_power_folders(path): subdirectories = [] driver_items = os.listdir(path) for item in driver_items: # All folder starting with: # - hwmon are JP5 compatible # - iio:device from JP4 or below if os.path.isdir(os.path.join(path, item)): driver_path = "{base_path}/{item}".format(base_path=path, item=item) if 'hwmon' in item: # Bring the second hwmon folder hwmon_name = os.listdir(driver_path)[0] driver_path = "{base_path}/{item}".format(base_path=driver_path, item=hwmon_name) subdirectories += [driver_path] elif 'iio:device' in item: subdirectories += [driver_path] return subdirectories def find_all_i2c_power_monitor(i2c_path): power_sensor = {} if not os.path.isdir(i2c_path): logger.error("Folder {root_dir} doesn't exist".format(root_dir=i2c_path)) return power_sensor power_i2c_sensors = {} items = os.listdir(i2c_path) for item in items: # Decode full path path = "{base_path}/{item}".format(base_path=i2c_path, item=item) name_path = "{path}/name".format(path=path) if os.path.isfile(name_path): raw_name = cat(name_path).strip() # Find all shunt and bus voltage monitor mounted on board # https://www.ti.com/product/INA3221 if 'ina3221' in raw_name: # Check which type of driver is working power_i2c_sensors[item] = find_driver_power_folders(path) # Build list of all power folder outputs # Find all voltage and current monitor for name, paths in power_i2c_sensors.items(): for path in paths: sensors = list_all_i2c_ports(path) power_sensor.update(sensors) if power_sensor: logger.info("Found I2C power monitor") return power_sensor def read_power_status(data): values = {} power_type = data['type'] try: for name, path in data.items(): if 'type' in name: continue elif 'status' in name: values[name] = cat(path).strip() continue elif 'online' in name: values[name] = cat(path).strip() == '1' continue # Fix from values with "ma" in the end, like # warn 32760 ma raw_value = int(cat(path).split(" ")[0]) values[name] = raw_value // 1000 if power_type != 'INA3221' else raw_value except OSError: values = {} return values def list_all_i2c_ports(path): sensor_name = {} # Build list label and path for item in os.listdir(path): power_label_path = "{path}/{item}".format(path=path, item=item) # Check if there is a label if item.endswith("_label"): # Decode name for JP 5 or above raw_name = cat(power_label_path).strip() # Remove NC power (Orin family) # https://docs.nvidia.com/jetson/archives/r34.1/DeveloperGuide/text/SD/PlatformPowerAndPerformance/JetsonOrinNxSeriesAndJetsonAgxOrinSeries.html#jetson-agx-orin-series if 'NC' in raw_name: logger.warn("Skipped NC {path}".format(path=power_label_path)) continue # Build list current and average power read number_port = int(item.split("_")[0].strip("in")) # Skip "sum of shunt voltages" always number 7 if number_port == 7: logger.warn("Skipped \"sum of shunt voltages\" {path}".format(path=power_label_path)) continue # Build list of path warnings = {'type': 'INA3221'} if check_file("{path}/curr{num}_crit_alarm".format(path=path, num=number_port)): warnings['crit_alarm'] = "{path}/curr{num}_crit_alarm".format(path=path, num=number_port) if check_file("{path}/curr{num}_max_alarm".format(path=path, num=number_port)): warnings['max_alarm'] = "{path}/curr{num}_max_alarm".format(path=path, num=number_port) values = read_power_status(warnings) logger.info("Alarms {name} - {data}".format(name=raw_name, data=values)) # Read Voltage, current and limits sensor = {'type': 'INA3221'} if check_file("{path}/in{num}_input".format(path=path, num=number_port)): sensor['volt'] = "{path}/in{num}_input".format(path=path, num=number_port) # Voltage in mV if check_file("{path}/curr{num}_input".format(path=path, num=number_port)): sensor['curr'] = "{path}/curr{num}_input".format(path=path, num=number_port) # Current in mA if check_file("{path}/curr{num}_max".format(path=path, num=number_port)): sensor['warn'] = "{path}/curr{num}_max".format(path=path, num=number_port) # in mA if check_file("{path}/curr{num}_crit".format(path=path, num=number_port)): sensor['crit'] = "{path}/curr{num}_crit".format(path=path, num=number_port) # in mA # If there is an file is added in list if len(sensor) > 1: sensor_name[raw_name] = sensor elif item.startswith("rail_name_"): # Decode name for JP 4 or previous raw_name = cat(power_label_path).strip() # Build list current and average power read number_port = int(item.lstrip("rail_name_")) # Build list of path # https://forums.developer.nvidia.com/t/jetson-tx1-ina226-power-monitor-with-i2c-interface/43819/5 sensor = {'type': 'INA3221'} if check_file("{path}/in_voltage{num}_input".format(path=path, num=number_port)): sensor['volt'] = "{path}/in_voltage{num}_input".format(path=path, num=number_port) # Voltage in mV if check_file("{path}/in_current{num}_input".format(path=path, num=number_port)): sensor['curr'] = "{path}/in_current{num}_input".format(path=path, num=number_port) # Current in mA if check_file("{path}/in_power{num}_input".format(path=path, num=number_port)): sensor['power'] = "{path}/in_power{num}_input".format(path=path, num=number_port) # Current in mW if check_file("{path}/warn_current_limit_{num}".format(path=path, num=number_port)): sensor['warn'] = "{path}/warn_current_limit_{num}".format(path=path, num=number_port) # in mA if check_file("{path}/crit_current_limit_{num}".format(path=path, num=number_port)): sensor['crit'] = "{path}/crit_current_limit_{num}".format(path=path, num=number_port) # in mA # If there is an file is added in list if len(sensor) > 1: sensor_name[raw_name] = sensor return sensor_name def find_all_system_monitor(system_monitor): sensor_name = {} if not os.path.isdir(system_monitor): logger.error("Folder {root_dir} doesn't exist".format(root_dir=system_monitor)) return sensor_name # Find all system power monitor for folder in os.listdir(system_monitor): local_path = "{path}/{folder}".format(path=system_monitor, folder=folder) name = folder.replace("ucsi-source-psy-", "") # Read type path_type = "{path}/type".format(path=local_path) type_supply = cat(path_type).strip() if check_file(path_type) else "SYSTEM" # Read model name path_name = "{path}/model_name".format(path=local_path) model_name = cat(path_name).strip() if check_file(path_name) else "" # Find power, current and voltage sensor = {'type': type_supply} # Status power if check_file("{path}/online".format(path=local_path)): sensor['online'] = "{path}/online".format(path=local_path) if check_file("{path}/status".format(path=local_path)): sensor['status'] = "{path}/status".format(path=local_path) if check_file("{path}/voltage_now".format(path=local_path)): sensor['volt'] = "{path}/voltage_now".format(path=local_path) # Voltage in mV if check_file("{path}/current_now".format(path=local_path)): sensor['curr'] = "{path}/current_now".format(path=local_path) # Current in mA if check_file("{path}/current_max".format(path=local_path)): sensor['warn'] = "{path}/current_max".format(path=local_path) # in mA # If there is an file is added in list if 'volt' in sensor and 'curr' in sensor: sensor_name[name] = sensor logger.info("Found name={name} type={type} model={model} in {path}".format( name=name, type=type_supply, model=model_name, path=folder)) else: logger.warning("Skipped {name} type={type} in={path}".format(name=name, type=type_supply, path=folder)) if sensor_name: logger.info("Found SYSTEM power monitor") return sensor_name class PowerService(object): def __init__(self): self._power_sensor = {} self._power_avg = {} # Find all I2C sensors on board i2c_path = "/sys/bus/i2c/devices" system_monitor = "/sys/class/power_supply" if os.getenv('JTOP_TESTING', False): i2c_path = "/fake_sys/bus/i2c/devices" system_monitor = "/fake_sys/class/power_supply" logger.warning("Running in JTOP_TESTING folder={root_dir}".format(root_dir=i2c_path)) logger.warning("Running in JTOP_TESTING folder={root_dir}".format(root_dir=system_monitor)) # Load all power sensors self._power_sensor = find_all_i2c_power_monitor(i2c_path) self._power_sensor.update(find_all_system_monitor(system_monitor)) if not self._power_sensor: logger.warning("Power sensors not found!") # Sort all power sensors self._power_sensor = dict(sorted(self._power_sensor.items(), key=lambda item: item[0])) def reset_avg_power(self): # Reset dictionary self._power_avg = {} def get_status(self): # If there are no sensors return an empty list if not self._power_sensor: return {} # Otherwise measure all values rails = {} for name, sensors in self._power_sensor.items(): # Read status sensors values = read_power_status(sensors) if not values: continue # Measure power if 'power' not in values: power = values['volt'] * (float(values['curr']) / 1000) values['power'] = int(power) # if online not in group (for I2C) if 'online' not in values: values['online'] = True # Measure average Power between first and previous interval if name in self._power_avg: old_avg = self._power_avg[name] values['avg'] = (values['power'] + old_avg) // 2 self._power_avg[name] = values['avg'] else: values['avg'] = values['power'] self._power_avg[name] = values['power'] # Add on power status rails[name] = values # Measure total power total, rails = total_power(rails) return {'rail': rails, 'tot': total} # EOF