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,
+)