| #include "tlbmc/sensors/psu_sensor.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <charconv> |
| #include <cstddef> |
| #include <cstdint> |
| #include <fstream> |
| #include <iterator> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <system_error> // NOLINT: system_error is commonly used in BMC |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "google/protobuf/duration.pb.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/ascii.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/substitute.h" |
| #include "boost/asio.hpp" // NOLINT: boost::asio is commonly used in BMC |
| #include "boost/asio/random_access_file.hpp" // NOLINT: boost::asio is commonly used in BMC |
| #include "boost/filesystem.hpp" // NOLINT: boost::filesystem is commonly used in BMC |
| #include "boost/filesystem/operations.hpp" // NOLINT: boost::filesystem is commonly used in BMC |
| #include "boost/system/detail/error_code.hpp" // NOLINT: boost::asio is commonly used in BMC |
| #include "g3/macros.h" |
| // NOLINTNEXTLINE: keep this so BUILD file will keep liburing |
| #include "liburing.h" // IWYU pragma: keep |
| #include "entity_common_config.pb.h" |
| #include "i2c_common_config.pb.h" |
| #include "psu_sensor_config.pb.h" |
| #include "reading_range_config.pb.h" |
| #include "threshold_config.pb.h" |
| #include "tlbmc/hal/sysfs/i2c.h" |
| #include "resource.pb.h" |
| #include "sensor.pb.h" |
| #include "tlbmc/sensors/i2c_hwmon_based_sensor.h" |
| #include "tlbmc/sensors/sensor.h" |
| #include "tlbmc/time/time.h" |
| |
| namespace milotic_tlbmc { |
| |
| template <class TypeNameFirst, class TypeNameSecond> |
| struct TypeComparator { |
| // Compared by the first element which is the type name. |
| bool operator()(const std::pair<TypeNameFirst, TypeNameSecond>& lhs, |
| const std::pair<TypeNameFirst, TypeNameSecond>& rhs) const { |
| return lhs.first < rhs.first; |
| } |
| }; |
| |
| constexpr std::array<std::pair<PsuSensorType, std::string_view>, 8> |
| kSupportedPsuSensorTypes = { |
| // go/keep-sorted start |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE1_ADM1266, "adm1266"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE2_ADM1272, "adm1272"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE3_PMBUS, "pmbus"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE4_LTC2991, "ltc2991"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE5_RAA228228, "raa228228"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE6_TDA38725, "tda38725"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE7_TDA38740, "tda38740"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE8_XDPE1A2G5B, "xdpe1a2g5b"}, |
| // go/keep-sorted end |
| }; |
| |
| // Refer to https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface and |
| // see OpenBMC definition of PSU Sensor Properties: |
| // https://github.com/openbmc/dbus-sensors/blob/af1724b84b7558037c665d2106ec44d7362aca6b/src/psu/PSUSensorMain.cpp#L1095 |
| constexpr std::array<std::pair<std::string_view, PsuSensor::ReadingProperties>, |
| 9> |
| kDefaultPsuSensorProperties = { |
| // go/keep-sorted start |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "highestpin", // input power |
| PsuSensor::ReadingProperties{.label_type_name = "Peak Input Power", |
| .max_reading = 3000.0, |
| .min_reading = 0.0, |
| .scale = 0.000001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "iin", // input current |
| PsuSensor::ReadingProperties{.label_type_name = "Input Current", |
| .max_reading = 20.0, |
| .min_reading = 0.0, |
| .scale = 0.001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "iout", // output current |
| PsuSensor::ReadingProperties{.label_type_name = "Output Current", |
| .max_reading = 255.0, |
| .min_reading = 0.0, |
| .scale = 0.001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "pin", // input power |
| PsuSensor::ReadingProperties{.label_type_name = "Input Power", |
| .max_reading = 3000.0, |
| .min_reading = 0.0, |
| .scale = 0.000001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "pout", // output power |
| PsuSensor::ReadingProperties{.label_type_name = "Output Power", |
| .max_reading = 3000.0, |
| .min_reading = 0.0, |
| .scale = 0.000001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "temp", // temperature |
| PsuSensor::ReadingProperties{.label_type_name = "Temperature", |
| .max_reading = 127.0, |
| .min_reading = -128.0, |
| .scale = 0.001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "vin", // input voltage |
| PsuSensor::ReadingProperties{.label_type_name = "Input Voltage", |
| .max_reading = 300.0, |
| .min_reading = 0.0, |
| .scale = 0.001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "voltage", // output voltage |
| PsuSensor::ReadingProperties{.label_type_name = "Output Voltage", |
| .max_reading = 255.0, |
| .min_reading = 0.0, |
| .scale = 0.001, |
| .offset = 0.0}}, |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| "vout", // output voltage |
| PsuSensor::ReadingProperties{.label_type_name = "Output Voltage", |
| .max_reading = 255.0, |
| .min_reading = 0.0, |
| .scale = 0.001, |
| .offset = 0.0}} |
| // go/keep-sorted end |
| }; |
| |
| constexpr std::array<std::pair<std::string_view, SensorUnit>, 5> |
| kInputWithoutDigitsToUnit = { |
| // go/keep-sorted start |
| std::pair<std::string_view, SensorUnit>{"curr", |
| SensorUnit::UNIT_AMPERE}, |
| std::pair<std::string_view, SensorUnit>{"in", SensorUnit::UNIT_VOLT}, |
| std::pair<std::string_view, SensorUnit>{"power", SensorUnit::UNIT_WATT}, |
| std::pair<std::string_view, SensorUnit>{ |
| "temp", SensorUnit::UNIT_DEGREE_CELSIUS}, |
| std::pair<std::string_view, SensorUnit>{"voltage", |
| SensorUnit::UNIT_VOLT}, |
| // go/keep-sorted end |
| }; |
| |
| absl::StatusOr<std::string_view> PsuSensor::GetDriverName( |
| PsuSensorType sensor_type) { |
| const auto* it = std::lower_bound( |
| kSupportedPsuSensorTypes.begin(), kSupportedPsuSensorTypes.end(), |
| std::pair<PsuSensorType, std::string_view>{sensor_type, ""}, |
| TypeComparator<PsuSensorType, std::string_view>()); |
| if (it == kSupportedPsuSensorTypes.end() || it->first != sensor_type) { |
| return absl::InvalidArgumentError( |
| absl::Substitute("Unsupported sensor type: $0", sensor_type)); |
| } |
| return it->second; |
| } |
| |
| absl::StatusOr<PsuSensor::ReadingProperties> PsuSensor::GetPsuSensorProperties( |
| std::string_view label) { |
| std::string_view label_no_digit = label; |
| std::size_t first_index = label_no_digit.find_first_of("0123456789"); |
| if (first_index != std::string_view::npos) { |
| label_no_digit = label_no_digit.substr(0, first_index); |
| } |
| const auto* it = std::lower_bound( |
| kDefaultPsuSensorProperties.begin(), kDefaultPsuSensorProperties.end(), |
| std::pair<std::string_view, PsuSensor::ReadingProperties>{ |
| label_no_digit, PsuSensor::ReadingProperties{}}, |
| TypeComparator<std::string_view, PsuSensor::ReadingProperties>()); |
| if (it == kDefaultPsuSensorProperties.end() || it->first != label_no_digit) { |
| return absl::InvalidArgumentError( |
| absl::Substitute("Unsupported sensor label: $0", label_no_digit)); |
| } |
| return it->second; |
| } |
| |
| absl::StatusOr<ReadingRangeConfigs> PsuSensor::GetReadingRangeConfigs( |
| const ReadingRangeConfigs& configs, const ReadingProperties& properties) { |
| ReadingRangeConfigs reading_range_configs = configs; |
| bool has_max_reading_range = false; |
| bool has_min_reading_range = false; |
| for (const auto& reading_range_config : |
| reading_range_configs.reading_range_configs()) { |
| if (reading_range_config.type() == READING_RANGE_TYPE_MAX) { |
| has_max_reading_range = true; |
| } else if (reading_range_config.type() == READING_RANGE_TYPE_MIN) { |
| has_min_reading_range = true; |
| } |
| } |
| // Add the default reading ranges if they are not specified in the config. |
| if (!has_max_reading_range) { |
| ReadingRangeConfig* reading_range = |
| reading_range_configs.add_reading_range_configs(); |
| reading_range->set_type(READING_RANGE_TYPE_MAX); |
| reading_range->set_reading(properties.max_reading); |
| } |
| if (!has_min_reading_range) { |
| ReadingRangeConfig* reading_range = |
| reading_range_configs.add_reading_range_configs(); |
| reading_range->set_type(READING_RANGE_TYPE_MIN); |
| reading_range->set_reading(properties.min_reading); |
| } |
| return reading_range_configs; |
| } |
| |
| absl::StatusOr<SensorUnit> PsuSensor::GetSensorUnit( |
| std::string_view input_file) { |
| std::string_view input_file_no_digit = input_file; |
| std::size_t first_index = input_file_no_digit.find_first_of("0123456789"); |
| if (first_index == std::string_view::npos) { |
| return absl::InvalidArgumentError(absl::Substitute( |
| "Failed to find the last non-digit character in $0", input_file)); |
| } |
| input_file_no_digit = input_file_no_digit.substr(0, first_index); |
| const auto* it = std::lower_bound( |
| kInputWithoutDigitsToUnit.begin(), kInputWithoutDigitsToUnit.end(), |
| std::pair<std::string_view, SensorUnit>{input_file_no_digit, |
| SensorUnit::UNIT_UNKNOWN}, |
| TypeComparator<std::string_view, SensorUnit>()); |
| if (it == kInputWithoutDigitsToUnit.end() || |
| it->first != input_file_no_digit) { |
| return absl::InvalidArgumentError(absl::Substitute( |
| "Unsupported sensor input file: $0", input_file_no_digit)); |
| } |
| return it->second; |
| } |
| |
| PsuSensorFileInfo PsuSensor::GetPsuSensorFileInfo(std::string_view input_file) { |
| for (int i = 0; i < kPsuSensorFileSuffixes.size(); ++i) { |
| std::string_view suffix = kPsuSensorFileSuffixes[i]; |
| if (absl::EndsWith(input_file, suffix)) { |
| return {.file_prefix = std::string( |
| input_file.substr(0, input_file.size() - suffix.size())), |
| .file_type = static_cast<PsuSensorFileType>(i)}; |
| } |
| } |
| return {.file_type = PsuSensorFileType::kUnknown}; |
| } |
| |
| absl::StatusOr<std::string> PsuSensor::ParseLabelFile( |
| const boost::filesystem::path& hwmon_path, std::string_view file_prefix) { |
| std::string label_file = absl::StrCat(file_prefix, kLabelSuffix); |
| // Both input file and label file should exist |
| boost::filesystem::path label_path = hwmon_path / label_file; |
| |
| if (!boost::filesystem::exists(hwmon_path / label_file)) { |
| return absl::InvalidArgumentError( |
| absl::Substitute("Failed to find label file $0", label_file)); |
| } |
| std::ifstream file(label_path.string()); |
| std::string label = {std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| return std::string(absl::StripTrailingAsciiWhitespace(label)); |
| } |
| |
| std::string PsuSensor::ApplyLabelModifications(std::string_view label, |
| PsuSensorFileType file_type) { |
| // For unknown file type, just return the label. |
| if (file_type == PsuSensorFileType::kUnknown) { |
| return std::string(label); |
| } |
| return absl::StrCat(kPsuSensorLabelPrefixes[static_cast<int>(file_type)], |
| label); |
| } |
| |
| std::string PsuSensor::GetLabel(const boost::filesystem::path& hwmon_path, |
| std::string_view file_prefix, |
| PsuSensorFileType file_type) { |
| // Try to find a label file and use its contents as the label. |
| // E.g. if the file prefix is "power1", then the label file should be at |
| // "power1_label". |
| // If the label file exists, then use the contents of the |
| // label file as the label. |
| // Otherwise, use the file_prefix as the label base. |
| absl::StatusOr<std::string> label = ParseLabelFile(hwmon_path, file_prefix); |
| if (!label.ok()) { |
| return ApplyLabelModifications(file_prefix, file_type); |
| } |
| return ApplyLabelModifications(*label, file_type); |
| } |
| |
| absl::StatusOr<std::vector<std::shared_ptr<Sensor>>> PsuSensor::Create( |
| const PsuSensorConfig& config, |
| const std::shared_ptr<boost::asio::io_context>& io_context, |
| const I2cSysfs& i2c_sysfs, std::optional<NotificationCb> on_batch_notify) { |
| DLOG(INFO) << "Creating PsuSensor for device: " << absl::StrCat(config); |
| const I2cCommonConfig& i2c_common_config = config.i2c_common_config(); |
| ECCLESIA_ASSIGN_OR_RETURN(std::string_view driver_name, |
| GetDriverName(config.type())); |
| |
| // E.g., /sys/bus/i2c/devices/20-0040/hwmon/hwmon18/ |
| ECCLESIA_ASSIGN_OR_RETURN( |
| boost::filesystem::path hwmon_path, |
| I2cHwmonBasedSensor::CreateDeviceAndReturnsHwmonPath( |
| i2c_common_config, driver_name, i2c_sysfs)); |
| |
| // Now find all labels and their input files. |
| std::unordered_map<std::string, std::string> label_to_input_file; |
| for (const auto& entry : boost::filesystem::directory_iterator(hwmon_path)) { |
| if (entry.is_regular_file()) { |
| std::string input_file = entry.path().filename().string(); |
| PsuSensorFileInfo file_info = GetPsuSensorFileInfo(input_file); |
| |
| // Ignore sensors that we don't support |
| if (file_info.file_type == PsuSensorFileType::kUnknown) { |
| continue; |
| } |
| |
| std::string label = |
| GetLabel(hwmon_path, file_info.file_prefix, file_info.file_type); |
| label_to_input_file[label] = input_file; |
| } |
| } |
| |
| std::vector<std::shared_ptr<Sensor>> sensors; |
| |
| for (const auto& [label, name] : config.label_to_name()) { |
| auto label_to_input_file_it = label_to_input_file.find(label); |
| if (label_to_input_file_it == label_to_input_file.end()) { |
| return absl::InvalidArgumentError(absl::Substitute( |
| "Failed to find $0 in hwmon folder $1", label, hwmon_path.string())); |
| } |
| const std::string& input_file = label_to_input_file_it->second; |
| boost::filesystem::path input_dev_path = hwmon_path / input_file; |
| |
| ThresholdConfigs threshold_configs; |
| if (auto it = config.label_to_thresholds().find(label); |
| it != config.label_to_thresholds().end()) { |
| threshold_configs = it->second; |
| } |
| |
| ReadingRangeConfigs reading_range_configs; |
| if (auto it = config.label_to_reading_ranges().find(label); |
| it != config.label_to_reading_ranges().end()) { |
| reading_range_configs = it->second; |
| } |
| |
| ECCLESIA_ASSIGN_OR_RETURN(PsuSensor::ReadingProperties properties, |
| GetPsuSensorProperties(label)); |
| ECCLESIA_ASSIGN_OR_RETURN( |
| reading_range_configs, |
| GetReadingRangeConfigs(reading_range_configs, properties)); |
| ECCLESIA_ASSIGN_OR_RETURN(SensorUnit sensor_unit, |
| GetSensorUnit(input_file)); |
| |
| std::string sensor_key = name; |
| if (sensor_key.empty()) { |
| // If sensor label name is not customized in the config, create key from |
| // prefix of sensor Name field and suffix of label_type_name |
| // https://github.com/openbmc/dbus-sensors/blob/556e04b8f374a9eb8cf32bf0e36ac46c14873eba/src/psu/PSUSensorMain.cpp#L934 |
| sensor_key = absl::StrCat(config.name(), "_", properties.label_type_name); |
| std::replace(sensor_key.begin(), sensor_key.end(), ' ', '_'); |
| } |
| switch (sensor_unit) { |
| case SensorUnit::UNIT_AMPERE: |
| sensor_key = absl::StrCat("current_", sensor_key); |
| break; |
| case SensorUnit::UNIT_VOLT: |
| sensor_key = absl::StrCat("voltage_", sensor_key); |
| break; |
| case SensorUnit::UNIT_WATT: |
| sensor_key = absl::StrCat("power_", sensor_key); |
| break; |
| case SensorUnit::UNIT_DEGREE_CELSIUS: |
| sensor_key = absl::StrCat("temperature_", sensor_key); |
| break; |
| default: |
| break; |
| } |
| |
| sensors.push_back(std::make_shared<PsuSensor>( |
| Token(), config.type(), sensor_unit, input_dev_path.string(), |
| sensor_key, config.i2c_common_config(), threshold_configs, |
| reading_range_configs, config.entity_common_config(), properties.scale, |
| properties.offset, io_context, on_batch_notify)); |
| } |
| |
| return sensors; |
| } |
| |
| void PsuSensor::HandleRefreshResult(const boost::system::error_code& error, |
| size_t bytes_read) { |
| if (error) { |
| State state; |
| state.set_status(STATUS_STALE); |
| state.set_status_message(absl::Substitute( |
| "Failed to read from input device: $0; input device path: $1", |
| error.message(), GetInputDevicePath())); |
| UpdateState(std::move(state)); |
| return; |
| } |
| const char* buffer_end = GetConstReadBuffer().data() + bytes_read; |
| uint64_t value = 0; |
| std::from_chars_result result = |
| std::from_chars(GetConstReadBuffer().data(), buffer_end, value); |
| if (result.ec != std::errc()) { |
| State state; |
| state.set_status(STATUS_STALE); |
| state.set_status_message( |
| absl::StrCat("Read data can't be converted to a number: ", |
| std::make_error_condition(result.ec).message())); |
| UpdateState(std::move(state)); |
| return; |
| } |
| SensorValue sensor_data; |
| *sensor_data.mutable_timestamp() = Now(); |
| sensor_data.set_reading((static_cast<double>(value) + offset_) * scale_); |
| StoreSensorData(std::make_shared<const SensorValue>(std::move(sensor_data))); |
| State state; |
| state.set_status(STATUS_READY); |
| UpdateState(std::move(state)); |
| } |
| |
| PsuSensor::PsuSensor(Token token, PsuSensorType sensor_type, |
| SensorUnit sensor_unit, const std::string& input_dev_path, |
| const std::string& sensor_name, |
| const I2cCommonConfig& i2c_common_config, |
| const ThresholdConfigs& threshold_configs, |
| const ReadingRangeConfigs& reading_range_configs, |
| const EntityCommonConfig& entity_common_config, |
| double scale, double offset, |
| const std::shared_ptr<boost::asio::io_context>& io_context, |
| std::optional<NotificationCb> on_batch_notify) |
| : I2cHwmonBasedSensor( |
| input_dev_path, io_context, |
| CreateStaticAttributes(sensor_name, sensor_unit, i2c_common_config, |
| entity_common_config, threshold_configs, |
| reading_range_configs), |
| on_batch_notify), |
| sensor_type_(sensor_type), |
| sensor_name_(sensor_name), |
| board_config_name_(entity_common_config.board_config_name()), |
| scale_(scale), |
| offset_(offset) {} |
| |
| } // namespace milotic_tlbmc |