| #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>, 2> |
| kSupportedPsuSensorTypes = { |
| // go/keep-sorted start |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE_ADM1266, "adm1266"}, |
| std::pair<PsuSensorType, std::string_view>{ |
| PsuSensorType::PSU_SENSOR_TYPE_ADM1272, "adm1272"}, |
| // 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>, |
| 5> |
| kDefaultPsuSensorProperties = { |
| // go/keep-sorted start |
| 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>{ |
| "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>{ |
| "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; |
| } |
| |
| 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)) { |
| constexpr std::string_view kInputFileSuffix = "_input"; |
| constexpr std::string_view kLabelSuffix = "_label"; |
| if (entry.is_regular_file() && |
| absl::EndsWith(entry.path().filename().string(), kInputFileSuffix)) { |
| std::string input_file = entry.path().filename().string(); |
| std::string file_prefix = |
| input_file.substr(0, input_file.size() - kInputFileSuffix.size()); |
| 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)) { |
| std::ifstream file(label_path.string()); |
| std::string label = {std::istreambuf_iterator<char>(file), |
| std::istreambuf_iterator<char>()}; |
| label = std::string(absl::StripTrailingAsciiWhitespace(label)); |
| label_to_input_file[label] = input_file; |
| } else if (boost::filesystem::exists(hwmon_path / input_file)) { |
| // If label file does not exist, use input file name prefix as label. |
| label_to_input_file[file_prefix] = 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, 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 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, 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 |