Add SMBPBI sensor
This sensor implementation polls readings from a virtual
eeprom i2c device. It can be used to read telemetry and
expose readings on dbus.
Entity Manager configs:
{
"Address": "0x1f",
"Bus": 1,
"ReadOffset": 288,
"Units": "DegreesC",
"Name": "Example_0_Temp_0",
"PollRate": 1.0,
"MinValue": -128,
"MaxValue": 127,
"Thresholds": [
{
"Direction": "greater than",
"Name": "upper non critical",
"Severity": 0,
"Value": 90.0
}
],
"ValueType": "UINT64",
"Type": "SmbpbiVirtualEeprom"
}
Change-Id: I13a5a82b583a31dd57feb7b3e6929e2a469d4b6d
Signed-off-by: Aushim Nagarkatti <anagarkatti@nvidia.com>
diff --git a/meson.options b/meson.options
index 306a953..00e6fa9 100644
--- a/meson.options
+++ b/meson.options
@@ -95,3 +95,9 @@
value: 'enabled',
description: 'Enable Liquid Leak Detector.',
)
+option(
+ 'smbpbi',
+ type: 'feature',
+ value: 'enabled',
+ description: 'Enable SMBPBI sensor.',
+)
diff --git a/service_files/meson.build b/service_files/meson.build
index 854cfd6..5a9938d 100644
--- a/service_files/meson.build
+++ b/service_files/meson.build
@@ -19,6 +19,7 @@
['nvidia-gpu', 'xyz.openbmc_project.nvidiagpusensor.service'],
['nvme', 'xyz.openbmc_project.nvmesensor.service'],
['psu', 'xyz.openbmc_project.psusensor.service'],
+ ['smbpbi', 'xyz.openbmc_project.smbpbisensor.service'],
]
fs = import('fs')
diff --git a/service_files/xyz.openbmc_project.smbpbisensor.service b/service_files/xyz.openbmc_project.smbpbisensor.service
new file mode 100644
index 0000000..88e839b
--- /dev/null
+++ b/service_files/xyz.openbmc_project.smbpbisensor.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=SMBPBI Sensor
+StopWhenUnneeded=false
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+Restart=always
+RestartSec=5
+ExecStart=/usr/bin/smbpbisensor
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/meson.build b/src/meson.build
index 2371d56..0929ba7 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -97,6 +97,10 @@
subdir('intel-cpu')
endif
+if get_option('smbpbi').allowed()
+ subdir('smbpbi')
+endif
+
if get_option('adc').allowed()
subdir('adc')
endif
diff --git a/src/smbpbi/SmbpbiSensor.cpp b/src/smbpbi/SmbpbiSensor.cpp
new file mode 100644
index 0000000..6b3aa1a
--- /dev/null
+++ b/src/smbpbi/SmbpbiSensor.cpp
@@ -0,0 +1,546 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "SmbpbiSensor.hpp"
+
+#include "SensorPaths.hpp"
+#include "Thresholds.hpp"
+#include "Utils.hpp"
+#include "sensor.hpp"
+
+#include <linux/i2c.h>
+
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/container/flat_map.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <array>
+#include <chrono>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+extern "C"
+{
+#include <linux/i2c-dev.h>
+#include <sys/ioctl.h>
+}
+
+constexpr const bool debug = false;
+
+constexpr const char* configInterface =
+ "xyz.openbmc_project.Configuration.SmbpbiVirtualEeprom";
+constexpr const char* sensorRootPath = "/xyz/openbmc_project/sensors/";
+constexpr const char* objectType = "SmbpbiVirtualEeprom";
+
+boost::container::flat_map<std::string, std::unique_ptr<SmbpbiSensor>> sensors;
+
+SmbpbiSensor::SmbpbiSensor(
+ std::shared_ptr<sdbusplus::asio::connection>& conn,
+ boost::asio::io_context& io, const std::string& sensorName,
+ const std::string& sensorConfiguration, const std::string& objType,
+ sdbusplus::asio::object_server& objectServer,
+ std::vector<thresholds::Threshold>&& thresholdData, uint8_t busId,
+ uint8_t addr, uint16_t offset, std::string& sensorUnits,
+ std::string& valueType, size_t pollTime, double minVal, double maxVal,
+ std::string& path) :
+ Sensor(escapeName(sensorName), std::move(thresholdData),
+ sensorConfiguration, objType, false, false, maxVal, minVal, conn),
+ busId(busId), addr(addr), offset(offset), sensorUnits(sensorUnits),
+ valueType(valueType), objectServer(objectServer),
+ inputDev(io, path, boost::asio::random_access_file::read_only),
+ waitTimer(io), pollRateSecond(pollTime)
+{
+ sensorType = sensor_paths::getPathForUnits(sensorUnits);
+ std::string sensorPath = sensorRootPath + sensorType + "/";
+
+ sensorInterface =
+ objectServer.add_interface(sensorPath + name, sensorValueInterface);
+
+ for (const auto& threshold : thresholds)
+ {
+ std::string interface = thresholds::getInterface(threshold.level);
+ thresholdInterfaces[static_cast<size_t>(threshold.level)] =
+ objectServer.add_interface(sensorPath + name, interface);
+ }
+ association =
+ objectServer.add_interface(sensorPath + name, association::interface);
+
+ if (sensorType == "temperature")
+ {
+ setInitialProperties(sensor_paths::unitDegreesC);
+ }
+ else if (sensorType == "power")
+ {
+ setInitialProperties(sensor_paths::unitWatts);
+ }
+ else if (sensorType == "energy")
+ {
+ setInitialProperties(sensor_paths::unitJoules);
+ }
+ else if (sensorType == "voltage")
+ {
+ setInitialProperties(sensor_paths::unitVolts);
+ }
+ else
+ {
+ lg2::error("no sensor type found");
+ }
+}
+
+SmbpbiSensor::~SmbpbiSensor()
+{
+ inputDev.close();
+ waitTimer.cancel();
+ for (const auto& iface : thresholdInterfaces)
+ {
+ objectServer.remove_interface(iface);
+ }
+ objectServer.remove_interface(sensorInterface);
+ objectServer.remove_interface(association);
+}
+
+void SmbpbiSensor::init()
+{
+ read();
+}
+
+void SmbpbiSensor::checkThresholds()
+{
+ thresholds::checkThresholds(this);
+}
+
+double SmbpbiSensor::convert2Temp(const uint8_t* raw)
+{
+ // Temp data is encoded in SMBPBI format. The 3 MSBs denote
+ // the integer portion, LSB is an encoded fraction.
+ // this automatic convert to int (two's complement integer)
+ int32_t intg = (raw[3] << 24 | raw[2] << 16 | raw[1] << 8 | raw[0]);
+ uint8_t frac = uint8_t(raw[0]);
+ // shift operation on a int keeps the sign in two's complement
+ intg >>= 8;
+
+ double temp = 0;
+ if (intg > 0)
+ {
+ temp = double(intg) + double(frac / 256.0);
+ }
+ else
+ {
+ temp = double(intg) - double(frac / 256.0);
+ }
+
+ return temp;
+}
+
+double SmbpbiSensor::convert2Power(const uint8_t* raw)
+{
+ // Power data is encoded as a 4-byte unsigned integer
+ uint32_t val = (raw[3] << 24) + (raw[2] << 16) + (raw[1] << 8) + raw[0];
+
+ // mWatts to Watts
+ double power = static_cast<double>(val) / 1000;
+
+ return power;
+}
+
+int SmbpbiSensor::i2cReadDataBytesDouble(double& reading)
+{
+ constexpr int length =
+ i2CReadLenValues[static_cast<size_t>(I2C_READ_LEN_INDEX::FLOAT64)];
+
+ static_assert(length == sizeof(reading), "Unsupported arch");
+
+ std::array<uint8_t, length> buf{};
+ int ret = i2cReadDataBytes(buf.data(), length);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ // there is no value updated from HMC if reading data is all 0xff
+ // Return NaN since reading is already a double
+ if (checkInvalidReading(buf.data(), length))
+ {
+ reading = std::numeric_limits<double>::quiet_NaN();
+ return 0;
+ }
+ uint64_t tempd = 0;
+ for (int byteI = 0; byteI < length; byteI++)
+ {
+ tempd |= static_cast<uint64_t>(buf[byteI]) << (8 * byteI);
+ }
+ std::memcpy(&reading, &tempd, sizeof(reading));
+
+ return 0;
+}
+
+int SmbpbiSensor::i2cReadDataBytesUI64(uint64_t& reading)
+{
+ constexpr int length =
+ i2CReadLenValues[static_cast<size_t>(I2C_READ_LEN_INDEX::UINT64)];
+
+ static_assert(length == sizeof(reading), "Unsupported arch");
+
+ std::array<uint8_t, length> buf{};
+ int ret = i2cReadDataBytes(buf.data(), length);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ reading = 0;
+ for (int byteI = 0; byteI < length; byteI++)
+ {
+ reading |= static_cast<uint64_t>(buf[byteI]) << (8 * byteI);
+ }
+ return 0;
+}
+
+// Generic i2c Command to read bytes
+int SmbpbiSensor::i2cReadDataBytes(uint8_t* reading, int length)
+{
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
+ const int fd = inputDev.native_handle();
+ if (fd < 0)
+ {
+ lg2::error(" unable to open i2c device on bus {BUS} err={FD}", "BUS",
+ busId, "FD", fd);
+ return -1;
+ }
+
+ unsigned long funcs = 0;
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
+ if (ioctl(fd, I2C_FUNCS, &funcs) < 0)
+ {
+ lg2::error(" I2C_FUNCS not supported");
+ return -1;
+ }
+
+ int ret = 0;
+ struct i2c_rdwr_ioctl_data args = {nullptr, 0};
+ struct i2c_msg msg = {0, 0, 0, nullptr};
+ std::array<uint8_t, 8> cmd{};
+
+ msg.addr = addr;
+ args.msgs = &msg;
+ args.nmsgs = 1;
+
+ msg.flags = 0;
+ msg.buf = cmd.data();
+ // handle two bytes offset
+ if (offset > 255)
+ {
+ msg.len = 2;
+ msg.buf[0] = offset >> 8;
+ msg.buf[1] = offset & 0xFF;
+ }
+ else
+ {
+ msg.len = 1;
+ msg.buf[0] = offset & 0xFF;
+ }
+
+ // write offset
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
+ ret = ioctl(fd, I2C_RDWR, &args);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ msg.flags = I2C_M_RD;
+ msg.len = length;
+ msg.buf = reading;
+
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
+ ret = ioctl(fd, I2C_RDWR, &args);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ return 0;
+}
+
+int SmbpbiSensor::readRawEEPROMData(double& data)
+{
+ uint64_t reading = 0;
+ int ret = i2cReadDataBytesUI64(reading);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+ if (checkInvalidReading(reinterpret_cast<uint8_t*>(&reading),
+ sizeof(reading)))
+ {
+ data = std::numeric_limits<double>::quiet_NaN();
+ return 0;
+ }
+ if (debug)
+ {
+ lg2::error("offset: {OFFSET} reading: {READING}", "OFFSET", offset,
+ "READING", reading);
+ }
+ if (sensorType == "temperature")
+ {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+ data = convert2Temp(reinterpret_cast<uint8_t*>(&reading));
+ }
+ else if (sensorType == "power")
+ {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+ data = convert2Power(reinterpret_cast<uint8_t*>(&reading));
+ }
+ else if (sensorType == "energy")
+ {
+ data = reading / 1000.0; // mJ to J (double)
+ }
+ else
+ {
+ data = reading; // Voltage
+ }
+ return 0;
+}
+
+int SmbpbiSensor::readFloat64EEPROMData(double& data)
+{
+ double reading = 0;
+ int ret = i2cReadDataBytesDouble(reading);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ data = reading;
+ return 0;
+}
+
+void SmbpbiSensor::waitReadCallback(const boost::system::error_code& ec)
+{
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ // we're being cancelled
+ return;
+ }
+ // read timer error
+ if (ec)
+ {
+ lg2::error("timer error");
+ return;
+ }
+ double temp = 0;
+
+ int ret = 0;
+ // Sensor reading value types are sensor-specific. So, read
+ // and interpret sensor data based on it's value type.
+ if (valueType == "UINT64")
+ {
+ ret = readRawEEPROMData(temp);
+ }
+ else if (valueType == "FLOAT64")
+ {
+ ret = readFloat64EEPROMData(temp);
+ }
+ else
+ {
+ return;
+ }
+
+ if (ret >= 0)
+ {
+ if constexpr (debug)
+ {
+ lg2::error("Value update to {TEMP}", "TEMP", temp);
+ }
+ updateValue(temp);
+ }
+ else
+ {
+ lg2::error("Invalid read getRegsInfo");
+ incrementError();
+ }
+ read();
+}
+
+void SmbpbiSensor::read()
+{
+ size_t pollTime = getPollRate(); // in seconds
+
+ waitTimer.expires_after(std::chrono::seconds(pollTime));
+ waitTimer.async_wait([this](const boost::system::error_code& ec) {
+ this->waitReadCallback(ec);
+ });
+}
+
+static void createSensorCallback(
+ boost::system::error_code ec, const ManagedObjectType& resp,
+ boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
+ std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
+ boost::container::flat_map<std::string, std::unique_ptr<SmbpbiSensor>>&
+ sensors)
+{
+ if (ec)
+ {
+ lg2::error("Error contacting entity manager");
+ return;
+ }
+ for (const auto& pathPair : resp)
+ {
+ for (const auto& entry : pathPair.second)
+ {
+ if (entry.first != configInterface)
+ {
+ continue;
+ }
+ std::string name = loadVariant<std::string>(entry.second, "Name");
+
+ std::vector<thresholds::Threshold> sensorThresholds;
+ if (!parseThresholdsFromConfig(pathPair.second, sensorThresholds))
+ {
+ lg2::error("error populating thresholds for {NAME}", "NAME",
+ name);
+ }
+
+ uint8_t busId = loadVariant<uint8_t>(entry.second, "Bus");
+
+ uint8_t addr = loadVariant<uint8_t>(entry.second, "Address");
+
+ uint16_t off = loadVariant<uint16_t>(entry.second, "ReadOffset");
+
+ std::string sensorUnits =
+ loadVariant<std::string>(entry.second, "Units");
+
+ std::string valueType =
+ loadVariant<std::string>(entry.second, "ValueType");
+ if (valueType != "UINT64" && valueType != "FLOAT64")
+ {
+ lg2::error("Invalid ValueType for sensor: {NAME}", "NAME",
+ name);
+ break;
+ }
+
+ size_t rate = loadVariant<uint8_t>(entry.second, "PollRate");
+
+ double minVal = loadVariant<double>(entry.second, "MinValue");
+
+ double maxVal = loadVariant<double>(entry.second, "MaxValue");
+ if constexpr (debug)
+ {
+ lg2::info("Configuration parsed for \n\t {CONF}\nwith\n"
+ "\tName: {NAME}\n"
+ "\tBus: {BUS}\n"
+ "\tAddress:{ADDR}\n"
+ "\tOffset: {OFF}\n"
+ "\tType : {TYPE}\n"
+ "\tValue Type : {VALUETYPE}\n"
+ "\tPollrate: {RATE}\n"
+ "\tMinValue: {MIN}\n"
+ "\tMaxValue: {MAX}\n",
+ "CONF", entry.first, "NAME", name, "BUS",
+ static_cast<int>(busId), "ADDR",
+ static_cast<int>(addr), "OFF", static_cast<int>(off),
+ "UNITS", sensorUnits, "VALUETYPE", valueType, "RATE",
+ rate, "MIN", minVal, "MAX", maxVal);
+ }
+
+ auto& sensor = sensors[name];
+ sensor = nullptr;
+
+ std::string path = "/dev/i2c-" + std::to_string(busId);
+
+ sensor = std::make_unique<SmbpbiSensor>(
+ dbusConnection, io, name, pathPair.first, objectType,
+ objectServer, std::move(sensorThresholds), busId, addr, off,
+ sensorUnits, valueType, rate, minVal, maxVal, path);
+
+ sensor->init();
+ }
+ }
+}
+
+void createSensors(
+ boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
+ boost::container::flat_map<std::string, std::unique_ptr<SmbpbiSensor>>&
+ sensors,
+ std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
+{
+ if (!dbusConnection)
+ {
+ lg2::error("Connection not created");
+ return;
+ }
+
+ dbusConnection->async_method_call(
+ [&io, &objectServer, &dbusConnection, &sensors](
+ boost::system::error_code ec, const ManagedObjectType& resp) {
+ createSensorCallback(ec, resp, io, objectServer, dbusConnection,
+ sensors);
+ },
+ entityManagerName, "/xyz/openbmc_project/inventory",
+ "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
+}
+
+int main()
+{
+ boost::asio::io_context io;
+ auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+ sdbusplus::asio::object_server objectServer(systemBus, true);
+ objectServer.add_manager("/xyz/openbmc_project/sensors");
+ systemBus->request_name("xyz.openbmc_project.SMBPBI");
+
+ boost::asio::post(io, [&]() {
+ createSensors(io, objectServer, sensors, systemBus);
+ });
+
+ boost::asio::steady_timer configTimer(io);
+
+ std::function<void(sdbusplus::message::message&)> eventHandler =
+ [&](sdbusplus::message::message&) {
+ configTimer.expires_after(std::chrono::seconds(1));
+ // create a timer because normally multiple properties change
+ configTimer.async_wait([&](const boost::system::error_code& ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ return; // we're being canceled
+ }
+ // config timer error
+ if (ec)
+ {
+ lg2::error("timer error");
+ return;
+ }
+ createSensors(io, objectServer, sensors, systemBus);
+ if (sensors.empty())
+ {
+ lg2::info("Configuration not detected");
+ }
+ });
+ };
+
+ sdbusplus::bus::match::match configMatch(
+ static_cast<sdbusplus::bus::bus&>(*systemBus),
+ "type='signal',member='PropertiesChanged',"
+ "path_namespace='" +
+ std::string(inventoryPath) +
+ "',"
+ "arg0namespace='" +
+ configInterface + "'",
+ eventHandler);
+
+ setupManufacturingModeMatch(*systemBus);
+ io.run();
+ return 0;
+}
diff --git a/src/smbpbi/SmbpbiSensor.hpp b/src/smbpbi/SmbpbiSensor.hpp
new file mode 100644
index 0000000..c869ce3
--- /dev/null
+++ b/src/smbpbi/SmbpbiSensor.hpp
@@ -0,0 +1,89 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+#include "Thresholds.hpp"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/random_access_file.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sensor.hpp>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+constexpr std::array<size_t, 3> i2CReadLenValues = {4, 8, 8};
+
+enum class I2C_READ_LEN_INDEX
+{
+ FLOAT32,
+ FLOAT64,
+ UINT64
+};
+
+struct SmbpbiSensor : public Sensor
+{
+ SmbpbiSensor(
+ std::shared_ptr<sdbusplus::asio::connection>& conn,
+ boost::asio::io_context& io, const std::string& name,
+ const std::string& sensorConfiguration, const std::string& objType,
+ sdbusplus::asio::object_server& objectServer,
+ std::vector<thresholds::Threshold>&& thresholdData, uint8_t busId,
+ uint8_t addr, uint16_t offset, std::string& sensorUnits,
+ std::string& valueType, size_t pollTime, double minVal, double maxVal,
+ std::string& path);
+ ~SmbpbiSensor() override;
+
+ void checkThresholds() override;
+
+ size_t getPollRate() const
+ {
+ return pollRateSecond;
+ }
+ void read();
+ void init();
+
+ uint8_t busId;
+ uint8_t addr;
+ uint16_t offset;
+ std::string sensorUnits;
+ std::string sensorType;
+ std::string valueType;
+
+ private:
+ int i2cReadDataBytes(uint8_t* reading, int length);
+ int i2cReadDataBytesDouble(double& reading);
+ int i2cReadDataBytesUI64(uint64_t& reading);
+ int readRawEEPROMData(double& data);
+ int readFloat64EEPROMData(double& data);
+ static double convert2Temp(const uint8_t* rawData);
+ static double convert2Power(const uint8_t* rawData);
+ void waitReadCallback(const boost::system::error_code& ec);
+ sdbusplus::asio::object_server& objectServer;
+ boost::asio::random_access_file inputDev;
+ boost::asio::steady_timer waitTimer;
+ size_t pollRateSecond;
+};
+
+bool checkInvalidReading(uint8_t* reading, int length)
+{
+ // there is no value updated from HMC if reading data is all 0xff
+ uint8_t* ptr = reading;
+ for (int i = 0; i < length; i++, ptr++)
+ {
+ if (*ptr != 0xFF)
+ {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/src/smbpbi/meson.build b/src/smbpbi/meson.build
new file mode 100644
index 0000000..5ca4c21
--- /dev/null
+++ b/src/smbpbi/meson.build
@@ -0,0 +1,9 @@
+src_inc = include_directories('..')
+
+executable(
+ 'smbpbisensor',
+ 'SmbpbiSensor.cpp',
+ dependencies: [default_deps, i2c, thresholds_dep, utils_dep],
+ include_directories: src_inc,
+ install: true,
+)