| #include "config.h" |
| |
| #include "sensor.hpp" |
| |
| #include "env.hpp" |
| #include "gpio_handle.hpp" |
| #include "hwmon.hpp" |
| #include "sensorset.hpp" |
| #include "sysfs.hpp" |
| |
| #include <phosphor-logging/elog-errors.hpp> |
| #include <xyz/openbmc_project/Common/error.hpp> |
| #include <xyz/openbmc_project/Sensor/Device/error.hpp> |
| |
| #include <cassert> |
| #include <chrono> |
| #include <cmath> |
| #include <cstring> |
| #include <filesystem> |
| #include <format> |
| #include <future> |
| #include <thread> |
| |
| namespace sensor |
| { |
| |
| using namespace phosphor::logging; |
| using namespace sdbusplus::xyz::openbmc_project::Common::Error; |
| |
| // todo: this can be simplified once we move to the double interface |
| Sensor::Sensor(const SensorSet::key_type& sensor, |
| const hwmonio::HwmonIOInterface* ioAccess, |
| const std::string& devPath) : |
| _sensor(sensor), _ioAccess(ioAccess), _devPath(devPath), _scale(0), |
| _hasFaultFile(false) |
| { |
| auto chip = env::getEnv("GPIOCHIP", sensor); |
| auto access = env::getEnv("GPIO", sensor); |
| if (!access.empty() && !chip.empty()) |
| { |
| _handle = gpio::BuildGpioHandle(chip, access); |
| |
| if (!_handle) |
| { |
| log<level::ERR>("Unable to set up gpio locking"); |
| elog<InternalFailure>(); |
| } |
| } |
| |
| auto gain = env::getEnv("GAIN", sensor); |
| if (!gain.empty()) |
| { |
| _sensorAdjusts.gain = std::stod(gain); |
| } |
| |
| auto offset = env::getEnv("OFFSET", sensor); |
| if (!offset.empty()) |
| { |
| _sensorAdjusts.offset = std::stoi(offset); |
| } |
| auto senRmRCs = env::getEnv("REMOVERCS", sensor); |
| // Add sensor removal return codes defined per sensor |
| addRemoveRCs(senRmRCs); |
| } |
| |
| void Sensor::addRemoveRCs(const std::string& rcList) |
| { |
| if (rcList.empty()) |
| { |
| return; |
| } |
| |
| // Convert to a char* for strtok |
| std::vector<char> rmRCs(rcList.c_str(), rcList.c_str() + rcList.size() + 1); |
| auto rmRC = std::strtok(&rmRCs[0], ", "); |
| while (rmRC != nullptr) |
| { |
| try |
| { |
| _sensorAdjusts.rmRCs.insert(std::stoi(rmRC)); |
| } |
| catch (const std::logic_error& le) |
| { |
| // Unable to convert to int, continue to next token |
| std::string name = _sensor.first + "_" + _sensor.second; |
| log<level::INFO>("Unable to convert sensor removal return code", |
| entry("SENSOR=%s", name.c_str()), |
| entry("RC=%s", rmRC), |
| entry("EXCEPTION=%s", le.what())); |
| } |
| rmRC = std::strtok(nullptr, ", "); |
| } |
| } |
| |
| SensorValueType Sensor::adjustValue(SensorValueType value) |
| { |
| // Because read doesn't have an out pointer to store errors. |
| // let's assume negative values are errors if they have this |
| // set. |
| #if NEGATIVE_ERRNO_ON_FAIL |
| if (value < 0) |
| { |
| return value; |
| } |
| #endif |
| |
| // Adjust based on gain and offset |
| value = static_cast<decltype(value)>( |
| static_cast<double>(value) * _sensorAdjusts.gain + |
| _sensorAdjusts.offset); |
| |
| if constexpr (std::is_same<SensorValueType, double>::value) |
| { |
| value *= std::pow(10, _scale); |
| } |
| |
| return value; |
| } |
| |
| std::shared_ptr<ValueObject> Sensor::addValue( |
| const RetryIO& retryIO, ObjectInfo& info, TimedoutMap& timedoutMap) |
| { |
| // Get the initial value for the value interface. |
| auto& bus = *std::get<sdbusplus::bus_t*>(info); |
| auto& obj = std::get<InterfaceMap>(info); |
| auto& objPath = std::get<std::string>(info); |
| |
| SensorValueType val = 0; |
| |
| auto& statusIface = std::any_cast<std::shared_ptr<StatusObject>&>( |
| obj[InterfaceType::STATUS]); |
| // As long as addStatus is called before addValue, statusIface |
| // should never be nullptr |
| assert(statusIface); |
| |
| // Only read the input value if the status is functional |
| if (statusIface->functional()) |
| { |
| #if UPDATE_FUNCTIONAL_ON_FAIL |
| try |
| #endif |
| { |
| // RAII object for GPIO unlock / lock |
| auto locker = gpioUnlock(getGpio()); |
| |
| // For sensors with attribute ASYNC_READ_TIMEOUT, |
| // spawn a thread with timeout |
| auto asyncReadTimeout = env::getEnv("ASYNC_READ_TIMEOUT", _sensor); |
| if (!asyncReadTimeout.empty()) |
| { |
| std::chrono::milliseconds asyncTimeout{ |
| std::stoi(asyncReadTimeout)}; |
| val = asyncRead(_sensor, _ioAccess, asyncTimeout, timedoutMap, |
| _sensor.first, _sensor.second, |
| hwmon::entry::cinput, std::get<size_t>(retryIO), |
| std::get<std::chrono::milliseconds>(retryIO)); |
| } |
| else |
| { |
| // Retry for up to a second if device is busy |
| // or has a transient error. |
| val = _ioAccess->read( |
| _sensor.first, _sensor.second, hwmon::entry::cinput, |
| std::get<size_t>(retryIO), |
| std::get<std::chrono::milliseconds>(retryIO)); |
| } |
| } |
| #if UPDATE_FUNCTIONAL_ON_FAIL |
| catch (const std::system_error& e) |
| { |
| // Catch the exception here and update the functional property. |
| // By catching the exception, it will not propagate it up the stack |
| // and thus the code will skip the "Remove RCs" check in |
| // MainLoop::getObject and will not exit on failure. |
| statusIface->functional(false); |
| } |
| #endif |
| } |
| |
| auto iface = std::make_shared<ValueObject>(bus, objPath.c_str(), |
| ValueObject::action::defer_emit); |
| |
| hwmon::Attributes attrs; |
| if (hwmon::getAttributes(_sensor.first, attrs)) |
| { |
| iface->unit(hwmon::getUnit(attrs)); |
| |
| _scale = hwmon::getScale(attrs); |
| } |
| |
| val = adjustValue(val); |
| iface->value(val); |
| |
| auto maxValue = env::getEnv("MAXVALUE", _sensor); |
| if (!maxValue.empty()) |
| { |
| iface->maxValue(std::stoll(maxValue)); |
| } |
| auto minValue = env::getEnv("MINVALUE", _sensor); |
| if (!minValue.empty()) |
| { |
| iface->minValue(std::stoll(minValue)); |
| } |
| |
| obj[InterfaceType::VALUE] = iface; |
| return iface; |
| } |
| |
| std::shared_ptr<StatusObject> Sensor::addStatus(ObjectInfo& info) |
| { |
| namespace fs = std::filesystem; |
| |
| std::shared_ptr<StatusObject> iface = nullptr; |
| auto& objPath = std::get<std::string>(info); |
| auto& obj = std::get<InterfaceMap>(info); |
| |
| // Check if fault sysfs file exists |
| std::string faultName = _sensor.first; |
| std::string faultID = _sensor.second; |
| std::string entry = hwmon::entry::fault; |
| |
| bool functional = true; |
| auto sysfsFullPath = |
| sysfs::make_sysfs_path(_ioAccess->path(), faultName, faultID, entry); |
| if (fs::exists(sysfsFullPath)) |
| { |
| _hasFaultFile = true; |
| try |
| { |
| uint32_t fault = _ioAccess->read(faultName, faultID, entry, |
| hwmonio::retries, hwmonio::delay); |
| if (fault != 0) |
| { |
| functional = false; |
| } |
| } |
| catch (const std::system_error& e) |
| { |
| using namespace sdbusplus::xyz::openbmc_project::Sensor::Device:: |
| Error; |
| using metadata = xyz::openbmc_project::Sensor::Device::ReadFailure; |
| |
| report<ReadFailure>( |
| metadata::CALLOUT_ERRNO(e.code().value()), |
| metadata::CALLOUT_DEVICE_PATH(_devPath.c_str())); |
| |
| log<level::INFO>(std::format("Failing sysfs file: {} errno {}", |
| sysfsFullPath, e.code().value()) |
| .c_str()); |
| } |
| } |
| |
| auto& bus = *std::get<sdbusplus::bus_t*>(info); |
| |
| iface = std::make_shared<StatusObject>( |
| bus, objPath.c_str(), StatusObject::action::emit_no_signals); |
| // Set functional property |
| iface->functional(functional); |
| |
| obj[InterfaceType::STATUS] = iface; |
| |
| return iface; |
| } |
| |
| std::shared_ptr<AccuracyObject> |
| Sensor::addAccuracy(ObjectInfo& info, double accuracy) |
| { |
| auto& objPath = std::get<std::string>(info); |
| auto& obj = std::get<InterfaceMap>(info); |
| |
| auto& bus = *std::get<sdbusplus::bus_t*>(info); |
| auto iface = std::make_shared<AccuracyObject>( |
| bus, objPath.c_str(), AccuracyObject::action::emit_no_signals); |
| |
| iface->accuracy(accuracy); |
| obj[InterfaceType::ACCURACY] = iface; |
| |
| return iface; |
| } |
| |
| std::shared_ptr<PriorityObject> |
| Sensor::addPriority(ObjectInfo& info, size_t priority) |
| { |
| auto& objPath = std::get<std::string>(info); |
| auto& obj = std::get<InterfaceMap>(info); |
| |
| auto& bus = *std::get<sdbusplus::bus_t*>(info); |
| auto iface = std::make_shared<PriorityObject>( |
| bus, objPath.c_str(), PriorityObject::action::emit_no_signals); |
| |
| iface->priority(priority); |
| obj[InterfaceType::PRIORITY] = iface; |
| |
| return iface; |
| } |
| |
| void gpioLock(const gpioplus::HandleInterface*&& handle) |
| { |
| handle->setValues({0}); |
| } |
| |
| std::optional<GpioLocker> gpioUnlock(const gpioplus::HandleInterface* handle) |
| { |
| if (handle == nullptr) |
| { |
| return std::nullopt; |
| } |
| |
| handle->setValues({1}); |
| // Default pause needed to guarantee sensors are ready |
| std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
| return GpioLocker(std::move(handle)); |
| } |
| |
| SensorValueType asyncRead( |
| const SensorSet::key_type& sensorSetKey, |
| const hwmonio::HwmonIOInterface* ioAccess, |
| std::chrono::milliseconds asyncTimeout, TimedoutMap& timedoutMap, |
| const std::string& type, const std::string& id, const std::string& sensor, |
| const size_t retries, const std::chrono::milliseconds delay) |
| { |
| // Default async read timeout |
| bool valueIsValid = false; |
| std::future<int64_t> asyncThread; |
| |
| auto asyncIter = timedoutMap.find(sensorSetKey); |
| if (asyncIter == timedoutMap.end()) |
| { |
| // If sensor not found in timedoutMap, spawn an async thread |
| asyncThread = |
| std::async(std::launch::async, &hwmonio::HwmonIOInterface::read, |
| ioAccess, type, id, sensor, retries, delay); |
| valueIsValid = true; |
| } |
| else |
| { |
| // If we already have the async thread in the timedoutMap, it means this |
| // sensor has already timed out in the previous reads. No need to wait |
| // on subsequent reads - proceed to check the future_status to see when |
| // the async thread finishes |
| asyncTimeout = std::chrono::seconds(0); |
| asyncThread = std::move(asyncIter->second); |
| } |
| |
| // TODO: This is still not a true asynchronous read as it still blocks the |
| // main thread for asyncTimeout amount of time. To make this completely |
| // asynchronous, schedule a read and register a callback to update the |
| // sensor value |
| std::future_status status = asyncThread.wait_for(asyncTimeout); |
| switch (status) |
| { |
| case std::future_status::ready: |
| // Read has finished |
| if (valueIsValid) |
| { |
| return asyncThread.get(); |
| // Good sensor reads should skip the code below |
| } |
| // Async read thread has completed but had previously timed out (was |
| // found in the timedoutMap). Erase from timedoutMap and throw to |
| // allow retry in the next read cycle. Not returning the read value |
| // as the sensor reading may be bad / corrupted if it took so long. |
| timedoutMap.erase(sensorSetKey); |
| throw AsyncSensorReadTimeOut(); |
| default: |
| // Read timed out so add the thread to the timedoutMap (if the entry |
| // already exists, operator[] updates it). |
| // |
| // Keeping the timed out futures in a map is required to prevent |
| // their destructor from being called when returning from this |
| // stack. The destructor will otherwise block until the read |
| // completes due to the limitation of std::async. |
| timedoutMap[sensorSetKey] = std::move(asyncThread); |
| throw AsyncSensorReadTimeOut(); |
| } |
| } |
| |
| } // namespace sensor |