| #include "tlbmc/sensors/adc_sensor.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <cerrno> |
| #include <chrono> // NOLINT: chrono is commonly used in BMC |
| #include <cstddef> |
| #include <cstdio> |
| #include <cstring> |
| #include <fstream> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "absl/functional/any_invocable.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/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "boost/asio/error.hpp" // NOLINT |
| #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 "g3/macros.h" |
| #include "tlbmc/central_config/config.h" |
| #include "adc_sensor_config.pb.h" |
| #include "entity_common_config.pb.h" |
| #include "hal_common_config.pb.h" |
| #include "reading_range_config.pb.h" |
| #include "reading_transform_config.pb.h" |
| #include "threshold_config.pb.h" |
| #include "tlbmc/hal/sysfs/i2c.h" |
| #include "tlbmc/hal/sysfs/iio.h" |
| #include "resource.pb.h" |
| #include "sensor.pb.h" |
| #include "tlbmc/sensors/polling_base_sensor.h" |
| #include "tlbmc/time/time.h" |
| |
| namespace milotic_tlbmc { |
| |
| namespace { |
| |
| constexpr auto kSupportedAdcSensorTypes = |
| std::to_array<std::pair<AdcSensorType, std::string_view>>({ |
| // go/keep-sorted start numeric=yes |
| std::pair<AdcSensorType, std::string_view>{ |
| AdcSensorType::ADC_SENSOR_TYPE1_TLA2024, "tla2024"}, |
| std::pair<AdcSensorType, std::string_view>{ |
| AdcSensorType::ADC_SENSOR_TYPE2_MAX11611, "max1363"}, |
| std::pair<AdcSensorType, std::string_view>{ |
| AdcSensorType::ADC_SENSOR_TYPE3_ADS1015, "ads1015"}, |
| std::pair<AdcSensorType, std::string_view>{ |
| AdcSensorType::ADC_SENSOR_TYPE4_MAX1363, "max1363"}, |
| std::pair<AdcSensorType, std::string_view>{ |
| AdcSensorType::ADC_SENSOR_TYPE5_ADS7142, "ads7142"}, |
| // go/keep-sorted end |
| }); |
| |
| } // namespace |
| |
| absl::StatusOr<std::shared_ptr<AdcSensor>> AdcSensor::Create( |
| const AdcSensorParameters& params, const IioSysfs& iio_sysfs, |
| const I2cSysfs& i2c_sysfs, std::optional<NotificationCb> on_batch_notify) { |
| SensorInstanceProperties mutable_instance_properties = |
| params.channel_config.instance_properties(); |
| if (!params.channel_config.instance_properties().has_related_item()) { |
| mutable_instance_properties.mutable_related_item()->set_type( |
| RESOURCE_TYPE_BOARD); |
| mutable_instance_properties.mutable_related_item()->set_id( |
| params.entity_common_config.board_config_key()); |
| } |
| mutable_instance_properties.set_name(absl::StrCat( |
| "voltage_", params.channel_config.instance_properties().name())); |
| |
| std::string full_path; |
| absl::Status status; |
| if (params.hal_common_config.has_bus() && |
| params.hal_common_config.has_address()) { |
| ECCLESIA_ASSIGN_OR_RETURN(std::string_view driver_name, |
| GetDriverName(params.type)); |
| auto device_base_path = CreateAdcDeviceAndReturnsHwmonPath( |
| params.hal_common_config, driver_name, i2c_sysfs); |
| if (!device_base_path.ok()) { |
| status = device_base_path.status(); |
| } else { |
| auto full_path_or = FindFullPathViaI2c(params, *device_base_path); |
| if (!full_path_or.ok()) { |
| status = full_path_or.status(); |
| } else { |
| full_path = *std::move(full_path_or); |
| } |
| } |
| } else { |
| auto full_path_or = FindFullPathViaIio(params, iio_sysfs); |
| if (!full_path_or.ok()) { |
| status = full_path_or.status(); |
| } else { |
| full_path = *std::move(full_path_or); |
| } |
| } |
| |
| if (!status.ok()) { |
| if (!GetTlbmcConfig() |
| .sensor_collector_module() |
| .allow_sensor_creation_failure() && |
| params.entity_common_config.config_detected()) { |
| return status; |
| } |
| |
| std::string error_message = absl::StrFormat( |
| "Failed to create AdcSensor (NONFATAL): %s: %s", |
| absl::StatusCodeToString(status.code()), status.message()); |
| return CreateFailedSensor(params, mutable_instance_properties, |
| on_batch_notify, error_message); |
| } |
| |
| auto sensor = std::make_shared<AdcSensor>(params, mutable_instance_properties, |
| full_path, on_batch_notify); |
| sensor->Init(); |
| return sensor; |
| } |
| |
| absl::StatusOr<std::shared_ptr<AdcSensor>> AdcSensor::CreateFailedSensor( |
| const AdcSensorParameters& params, |
| const SensorInstanceProperties& instance_properties, |
| std::optional<NotificationCb> on_batch_notify, |
| std::string_view error_message) { |
| auto sensor = std::make_shared<AdcSensor>(params, instance_properties, "", |
| on_batch_notify); |
| State state; |
| if (!params.entity_common_config.config_detected()) { |
| state.set_status(STATUS_CREATION_PENDING); |
| } else { |
| state.set_status(STATUS_CREATION_FAILED); |
| } |
| state.set_status_message(error_message); |
| sensor->UpdateState(std::move(state)); |
| return sensor; |
| } |
| |
| AdcSensor::AdcSensor(const AdcSensorParameters& params, |
| const SensorInstanceProperties& instance_properties, |
| const std::string& full_path, |
| std::optional<NotificationCb> on_batch_notify) |
| : PollingBaseSensor( |
| CreateStaticAttributes(instance_properties, params.hal_common_config, |
| params.entity_common_config), |
| instance_properties.thresholds(), on_batch_notify), |
| device_name_(params.device_name), |
| file_name_(params.channel_config.file_name()), |
| gpio_threshold_millivolts_( |
| params.channel_config.has_gpio_threshold_millivolts() |
| ? std::make_optional( |
| params.channel_config.gpio_threshold_millivolts()) |
| : std::nullopt), |
| io_context_(params.io_context), |
| sd_input_device_(*io_context_), |
| full_path_(full_path) { |
| absl::StatusOr<double> scale_value = GetFileScaleValue(); |
| if (scale_value.ok()) { |
| scale_value_ = *scale_value; |
| } else { |
| LOG(WARNING) << "Failed to get scale value for " << device_name_ << ": " |
| << scale_value.status().message() |
| << ". Using default scale 1.0."; |
| } |
| |
| if (params.channel_config.instance_properties() |
| .has_reading_transform_config() && |
| params.channel_config.instance_properties() |
| .reading_transform_config() |
| .has_scale()) { |
| scale_value_ = params.channel_config.instance_properties() |
| .reading_transform_config() |
| .scale(); |
| } |
| |
| if (scale_value_ == 0) { |
| LOG(ERROR) << "Scale value is 0 for " << device_name_ |
| << "from either file or config. Resetting scale value to 1.0."; |
| scale_value_ = 1.0; |
| } |
| } |
| |
| void AdcSensor::Init() { |
| if (sd_input_device_.is_open()) { |
| LOG(INFO) << "ADC device already opened: " << full_path_; |
| return; |
| } |
| if (full_path_.empty()) { |
| return; |
| } |
| int fd = open(full_path_.c_str(), O_RDONLY); |
| if (fd >= 0) { |
| sd_input_device_.assign(fd); |
| } else { |
| LOG(ERROR) << "Failed to open ADC device: " << full_path_ << " (fd: " << fd |
| << ")"; |
| } |
| } |
| |
| AdcSensor::~AdcSensor() { |
| boost::system::error_code ec; |
| if (sd_input_device_.is_open()) { |
| boost::system::error_code ec_close = sd_input_device_.close(ec); |
| if (ec_close) { |
| LOG(WARNING) << "Failed to close sd_input_device_: " |
| << ec_close.message(); |
| } |
| } |
| } |
| |
| void AdcSensor::HandleReadDone( |
| const boost::system::error_code& error, size_t bytes_read, |
| absl::AnyInvocable<void(const std::shared_ptr<const SensorValue>&)> |
| callback) { |
| if (error) { |
| if (error == boost::asio::error::no_such_device || |
| error == boost::system::errc::no_such_file_or_directory || |
| error == boost::system::errc::no_such_device_or_address || |
| error == boost::system::errc::bad_file_descriptor) { |
| boost::system::error_code ec_close; |
| if (sd_input_device_.is_open()) { |
| ec_close = sd_input_device_.close(ec_close); |
| if (ec_close) { |
| LOG(WARNING) << "Failed to close stale sd_input_device_: " |
| << ec_close.message(); |
| } |
| } |
| } |
| State state; |
| state.set_status(STATUS_STALE); |
| state.set_status_message(absl::StrCat( |
| "Failed to read sensor value with error: ", error.message())); |
| UpdateState(std::move(state)); |
| if (callback) { |
| callback(GetSensorData()); |
| } |
| return; |
| } |
| |
| double reading = 0; |
| const std::string_view str_value(read_buffer_.data(), bytes_read); |
| if (!absl::SimpleAtod(str_value, &reading)) { |
| LOG(ERROR) << "Failed to parse sensor reading: " << str_value; |
| State state; |
| state.set_status(STATUS_STALE); |
| state.set_status_message("Failed to parse sensor reading"); |
| UpdateState(std::move(state)); |
| if (callback) { |
| callback(GetSensorData()); |
| } |
| return; |
| } |
| |
| // The scale value is already validated in the constructor to be |
| // non-zero. |
| reading /= scale_value_; |
| |
| if (gpio_threshold_millivolts_.has_value()) { |
| LOG(INFO) << "GPIO threshold exists, inferring virtual gpio value " |
| "based on the threshold and store it."; |
| auto processed_sensor_value = std::make_shared<SensorValue>(); |
| *processed_sensor_value->mutable_timestamp() = Now(); |
| processed_sensor_value->set_reading( |
| reading > static_cast<double>(*gpio_threshold_millivolts_) ? 1.0 : 0.0); |
| StoreSensorData(processed_sensor_value); |
| } else { |
| LOG(INFO) << "GPIO threshold does not exist, storing raw sensor value."; |
| auto sensor_value = std::make_shared<SensorValue>(); |
| *sensor_value->mutable_timestamp() = Now(); |
| sensor_value->set_reading(reading); |
| StoreSensorData(sensor_value); |
| } |
| State state; |
| state.set_status(STATUS_READY); |
| UpdateState(std::move(state)); |
| |
| if (callback) { |
| LOG(INFO) << "Calling callback to return sensor data"; |
| callback(GetSensorData()); |
| } |
| } |
| |
| void AdcSensor::RefreshOnceAsync( |
| absl::AnyInvocable<void(const std::shared_ptr<const SensorValue>&)> |
| callback) { |
| std::weak_ptr<AdcSensor> weak_self = shared_from_this(); |
| |
| auto on_read_done = [weak_self, callback = std::move(callback)]( |
| const boost::system::error_code& error, |
| size_t bytes_read) mutable { |
| std::shared_ptr<AdcSensor> self = weak_self.lock(); |
| if (!self) { |
| LOG(WARNING) << "Sensor is destroyed; cancel the refresh."; |
| return; |
| } |
| self->HandleReadDone(error, bytes_read, std::move(callback)); |
| std::chrono::steady_clock::time_point last_refresh_end_time = |
| self->GetLastRefreshEndTime(); |
| std::chrono::steady_clock::time_point current_time_point_2 = |
| std::chrono::steady_clock::now(); |
| if (last_refresh_end_time != std::chrono::steady_clock::time_point::min()) { |
| self->UpdateSoftwarePollingEndMetrics(current_time_point_2 - |
| last_refresh_end_time); |
| } |
| self->SetLastRefreshEndTime(current_time_point_2); |
| }; |
| |
| std::shared_ptr<AdcSensor> self = weak_self.lock(); |
| if (!self) { |
| LOG(WARNING) << "Sensor is destroyed; cancel the refresh."; |
| return; |
| } |
| std::chrono::steady_clock::time_point last_refresh_start_time = |
| self->GetLastRefreshStartTime(); |
| std::chrono::steady_clock::time_point current_time_point_1 = |
| std::chrono::steady_clock::now(); |
| if (last_refresh_start_time != std::chrono::steady_clock::time_point::min()) { |
| self->UpdateSoftwarePollingStartMetrics(current_time_point_1 - |
| last_refresh_start_time); |
| } |
| self->SetLastRefreshStartTime(current_time_point_1); |
| |
| if (!self->sd_input_device_.is_open()) { |
| self->Init(); |
| } |
| |
| if (self->sd_input_device_.is_open()) { |
| if (lseek(self->sd_input_device_.native_handle(), 0, SEEK_SET) == -1) { |
| int err = errno; |
| LOG(WARNING) << "lseek failed on " << self->full_path_ |
| << " with error: " << strerror(err); |
| if (err == ENODEV || err == EBADF || err == ENOENT || err == ENXIO) { |
| boost::system::error_code ec_close; |
| ec_close = self->sd_input_device_.close(ec_close); |
| if (ec_close) { |
| LOG(WARNING) << "Failed to close stale sd_input_device_: " |
| << ec_close.message(); |
| } |
| } |
| const boost::system::error_code ec = boost::system::errc::make_error_code( |
| static_cast<boost::system::errc::errc_t>(err)); |
| constexpr size_t bytes_read = 0; |
| boost::asio::post(*self->io_context_, |
| [on_read_done = std::move(on_read_done), ec]() mutable { |
| on_read_done(ec, bytes_read); |
| }); |
| return; |
| } |
| self->sd_input_device_.async_read_some( |
| boost::asio::buffer(self->read_buffer_), |
| [on_read_done = std::move(on_read_done)]( |
| const boost::system::error_code& ec, |
| std::size_t bytes_transferred) mutable { |
| if (ec == boost::system::errc::bad_file_descriptor) { |
| LOG(INFO) << "AdcSensor: sd_input_device_ is closed."; |
| return; |
| } |
| on_read_done(ec, bytes_transferred); |
| }); |
| return; |
| } |
| |
| const boost::system::error_code ec = boost::system::errc::make_error_code( |
| boost::system::errc::no_such_file_or_directory); |
| constexpr size_t bytes_read = 0; |
| boost::asio::post(*self->io_context_, |
| [on_read_done = std::move(on_read_done), ec]() mutable { |
| on_read_done(ec, bytes_read); |
| }); |
| } |
| |
| absl::StatusOr<double> AdcSensor::GetFileScaleValue() const { |
| auto scale_file_path = GetFileScalePath(); |
| if (!scale_file_path.ok()) { |
| return scale_file_path.status(); |
| } |
| |
| std::ifstream scale_file(*scale_file_path); |
| if (!scale_file.is_open()) { |
| return absl::UnavailableError( |
| absl::StrCat("Could not open scale file: ", *scale_file_path)); |
| } |
| |
| std::string content; |
| std::getline(scale_file, content); |
| absl::StripTrailingAsciiWhitespace(&content); |
| |
| double scale_value; |
| if (!absl::SimpleAtod(content, &scale_value)) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Could not parse scale value: ", content)); |
| } |
| |
| return scale_value; |
| } |
| |
| absl::StatusOr<std::string> AdcSensor::GetFileScalePath() const { |
| std::string scale_file_name = "in_voltage_scale"; |
| if (absl::StrContains(file_name_, "-voltage")) { |
| scale_file_name = "in_voltage-voltage_scale"; |
| } |
| return (boost::filesystem::path(full_path_).parent_path() / scale_file_name) |
| .string(); |
| } |
| |
| absl::StatusOr<std::string_view> AdcSensor::GetDriverName( |
| AdcSensorType sensor_type) { |
| const auto* it = std::lower_bound( |
| kSupportedAdcSensorTypes.begin(), kSupportedAdcSensorTypes.end(), |
| std::pair<AdcSensorType, std::string_view>{sensor_type, ""}, |
| [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); |
| if (it == kSupportedAdcSensorTypes.end() || it->first != sensor_type) { |
| return absl::InvalidArgumentError(absl::StrCat( |
| "Unsupported sensor type: ", static_cast<int>(sensor_type))); |
| } |
| return it->second; |
| } |
| |
| absl::StatusOr<boost::filesystem::path> |
| AdcSensor::CreateAdcDeviceAndReturnsHwmonPath(const HalCommonConfig& hal_config, |
| std::string_view driver_name, |
| const I2cSysfs& i2c_sysfs) { |
| if (absl::Status status = i2c_sysfs.NewDevice(hal_config, driver_name); |
| !status.ok() && status.code() != absl::StatusCode::kAlreadyExists) { |
| return status; |
| } |
| |
| if (!i2c_sysfs.IsDevicePresent(hal_config)) { |
| return absl::NotFoundError( |
| absl::StrCat("Failed to find device ", hal_config.bus(), "-", |
| hal_config.address(), " after creating it")); |
| } |
| |
| boost::filesystem::path device_path = |
| i2c_sysfs.GetBusPath(hal_config.bus()) / |
| i2c_sysfs.GetDeviceDirectoryName(hal_config); |
| |
| return device_path; |
| } |
| |
| absl::StatusOr<std::string> AdcSensor::FindFullPathViaI2c( |
| const AdcSensorParameters& params, |
| const boost::filesystem::path& device_base_path) { |
| boost::system::error_code ec; |
| if (!boost::filesystem::is_directory(device_base_path, ec) || ec) { |
| LOG(ERROR) << "Failed to find I2C device directory: " << device_base_path; |
| return absl::NotFoundError(absl::StrCat("I2C device directory not found: ", |
| device_base_path.string())); |
| } |
| |
| boost::filesystem::path sensor_dir; |
| bool found = false; |
| for (auto iter = boost::filesystem::directory_iterator(device_base_path, ec); |
| iter != boost::filesystem::directory_iterator(); iter.increment(ec)) { |
| const auto& entry = *iter; |
| if (absl::StartsWith(entry.path().filename().string(), "iio")) { |
| sensor_dir = entry.path(); |
| found = true; |
| break; |
| } |
| } |
| |
| if (ec) { |
| LOG(ERROR) << "Failed to iterate IIO directory under " << device_base_path |
| << ": " << ec.message(); |
| return absl::InternalError( |
| absl::StrCat("Failed to iterate IIO directory: ", ec.message())); |
| } |
| |
| if (!found) { |
| LOG(ERROR) << "Failed to find IIO directory under " << device_base_path; |
| return absl::NotFoundError(absl::StrCat("IIO directory not found under ", |
| device_base_path.string())); |
| } |
| |
| boost::filesystem::path sensor_path = |
| sensor_dir / params.channel_config.file_name(); |
| if (!boost::filesystem::exists(sensor_path, ec) || ec) { |
| LOG(ERROR) << "Failed to find sensor file: " << sensor_path; |
| return absl::NotFoundError( |
| absl::StrCat("Sensor file not found: ", sensor_path.string())); |
| } |
| return sensor_path.string(); |
| } |
| |
| absl::StatusOr<std::string> AdcSensor::FindFullPathViaIio( |
| const AdcSensorParameters& params, const IioSysfs& iio_sysfs) { |
| const absl::StatusOr<boost::filesystem::path> sensor_path = |
| iio_sysfs.GetSensorPath(params.device_name, |
| params.channel_config.file_name()); |
| if (!sensor_path.ok()) { |
| LOG(ERROR) << "Failed to get sensor path for " << params.device_name << " " |
| << params.channel_config.file_name(); |
| return sensor_path.status(); |
| } |
| return sensor_path->string(); |
| } |
| |
| } // namespace milotic_tlbmc |