nvidia-gpu: add support for communication to the endpoint

The commit uses MCTP VDM protocol to read temperature sensor value from
the gpu.

The MCTP VDM protocol is an extension of the OCP Accelerator Management
Interface specification. [1]

Tested: Build an image for gb200nvl-obmc machine with the following
patches cherry picked. This patches are needed to enable the mctp stack.

https://gerrit.openbmc.org/c/openbmc/openbmc/+/79422

Restart the nvidiagpusensor service.
```
root@gb200nvl-obmc:~# systemctl start xyz.openbmc_project.nvidiagpusensor.service
```

The app is detecting entity-manager configuration on gb200nvl-obmc
machine. The app is also able to detect all the endpoints from the mctp
service dbus tree. The app is reading temperature sensor value from gpu
correctly and the temperature sensor is also present on redfish.

```
$ curl -k -u 'root:0penBmc' https://10.137.203.137/redfish/v1/Chassis/NVIDIA_GB200_1/Sensors/temperature_NVIDIA_GB200_GPU
{
  "@odata.id": "/redfish/v1/Chassis/NVIDIA_GB200_1/Sensors/temperature_NVIDIA_GB200_GPU",
  "@odata.type": "#Sensor.v1_2_0.Sensor",
  "Id": "temperature_NVIDIA_GB200_GPU",
  "Name": "NVIDIA GB200 GPU",
  "Reading": 36.4375,
  "ReadingRangeMax": 127.0,
  "ReadingRangeMin": -128.0,
  "ReadingType": "Temperature",
  "ReadingUnits": "Cel",
  "Status": {
    "Health": "OK",
    "State": "Enabled"
  }
}%

root@gb200nvl-obmc:~# busctl tree xyz.openbmc_project.GpuSensor
└─ /xyz
  └─ /xyz/openbmc_project
    └─ /xyz/openbmc_project/sensors
      └─ /xyz/openbmc_project/sensors/temperature
        └─ /xyz/openbmc_project/sensors/temperature/NVIDIA_GB200_GPU

root@gb200nvl-obmc:~# busctl introspect xyz.openbmc_project.GpuSensor /xyz/openbmc_project/sensors/temperature/NVIDIA_GB200_GPU
NAME                                                  TYPE      SIGNATURE RESULT/VALUE                             FLAGS
org.freedesktop.DBus.Introspectable                   interface -         -                                        -
.Introspect                                           method    -         s                                        -
org.freedesktop.DBus.Peer                             interface -         -                                        -
.GetMachineId                                         method    -         s                                        -
.Ping                                                 method    -         -                                        -
org.freedesktop.DBus.Properties                       interface -         -                                        -
.Get                                                  method    ss        v                                        -
.GetAll                                               method    s         a{sv}                                    -
.Set                                                  method    ssv       -                                        -
.PropertiesChanged                                    signal    sa{sv}as  -                                        -
xyz.openbmc_project.Association.Definitions           interface -         -                                        -
.Associations                                         property  a(sss)    1 "chassis" "all_sensors" "/xyz/openbmc… emits-change
xyz.openbmc_project.Sensor.Value                      interface -         -                                        -
.MaxValue                                             property  d         127                                      emits-change
.MinValue                                             property  d         -128                                     emits-change
.Unit                                                 property  s         "xyz.openbmc_project.Sensor.Value.Unit.… emits-change
.Value                                                property  d         36.3125                                  emits-change writable
xyz.openbmc_project.Sensor.ValueMutability            interface -         -                                        -
.Mutable                                              property  b         true                                     emits-change
xyz.openbmc_project.State.Decorator.Availability      interface -         -                                        -
.Available                                            property  b         true                                     emits-change writable
xyz.openbmc_project.State.Decorator.OperationalStatus interface -         -                                        -
.Functional                                           property  b         true                                     emits-change
```

[1] https://www.opencompute.org/documents/ocp-gpu-accelerator-management-interfaces-v1-pdf

Change-Id: Ied938b9e5c19751ee283b4b948e16c905c78fb48
Signed-off-by: Harshit Aghera <haghera@nvidia.com>
diff --git a/src/nvidia-gpu/MctpRequester.cpp b/src/nvidia-gpu/MctpRequester.cpp
new file mode 100644
index 0000000..024f8cc
--- /dev/null
+++ b/src/nvidia-gpu/MctpRequester.cpp
@@ -0,0 +1,162 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "MctpRequester.hpp"
+
+#include <linux/mctp.h>
+#include <sys/socket.h>
+
+#include <OcpMctpVdm.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/generic/datagram_protocol.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <cerrno>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <span>
+#include <utility>
+
+using namespace std::literals;
+
+namespace mctp
+{
+
+MctpRequester::MctpRequester(boost::asio::io_context& ctx) :
+    mctpSocket(ctx, boost::asio::generic::datagram_protocol{AF_MCTP, 0}),
+    expiryTimer(ctx)
+{}
+
+void MctpRequester::processRecvMsg(
+    uint8_t eid, const std::span<const uint8_t> reqMsg,
+    const std::span<uint8_t> respMsg, const boost::system::error_code& ec,
+    const size_t /*length*/)
+{
+    expiryTimer.cancel();
+
+    if (ec)
+    {
+        lg2::error(
+            "MctpRequester failed to receive data from the MCTP socket - ErrorCode={EC}, Error={ER}.",
+            "EC", ec.value(), "ER", ec.message());
+        completionCallback(EIO);
+        return;
+    }
+
+    const auto* respAddr =
+        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+        reinterpret_cast<const struct sockaddr_mctp*>(recvEndPoint.data());
+
+    if (respAddr->smctp_type != msgType)
+    {
+        lg2::error("MctpRequester: Message type mismatch");
+        completionCallback(EPROTO);
+        return;
+    }
+
+    uint8_t respEid = respAddr->smctp_addr.s_addr;
+
+    if (respEid != eid)
+    {
+        lg2::error(
+            "MctpRequester: EID mismatch - expected={EID}, received={REID}",
+            "EID", eid, "REID", respEid);
+        completionCallback(EPROTO);
+        return;
+    }
+
+    if (respMsg.size() > sizeof(ocp::accelerator_management::BindingPciVid))
+    {
+        const auto* reqHdr =
+            // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+            reinterpret_cast<const ocp::accelerator_management::BindingPciVid*>(
+                reqMsg.data());
+
+        uint8_t reqInstanceId = reqHdr->instance_id &
+                                ocp::accelerator_management::instanceIdBitMask;
+        const auto* respHdr =
+            // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+            reinterpret_cast<const ocp::accelerator_management::BindingPciVid*>(
+                respMsg.data());
+
+        uint8_t respInstanceId = respHdr->instance_id &
+                                 ocp::accelerator_management::instanceIdBitMask;
+
+        if (reqInstanceId != respInstanceId)
+        {
+            lg2::error(
+                "MctpRequester: Instance ID mismatch - request={REQ}, response={RESP}",
+                "REQ", static_cast<int>(reqInstanceId), "RESP",
+                static_cast<int>(respInstanceId));
+            completionCallback(EPROTO);
+            return;
+        }
+    }
+
+    completionCallback(0);
+}
+
+void MctpRequester::handleSendMsgCompletion(
+    uint8_t eid, const std::span<const uint8_t> reqMsg,
+    std::span<uint8_t> respMsg, const boost::system::error_code& ec,
+    size_t /* length */)
+{
+    if (ec)
+    {
+        lg2::error(
+            "MctpRequester failed to send data from the MCTP socket - ErrorCode={EC}, Error={ER}.",
+            "EC", ec.value(), "ER", ec.message());
+        completionCallback(EIO);
+        return;
+    }
+
+    expiryTimer.expires_after(2s);
+
+    expiryTimer.async_wait([this](const boost::system::error_code& ec) {
+        if (ec != boost::asio::error::operation_aborted)
+        {
+            completionCallback(ETIME);
+        }
+    });
+
+    mctpSocket.async_receive_from(
+        boost::asio::mutable_buffer(respMsg), recvEndPoint,
+        std::bind_front(&MctpRequester::processRecvMsg, this, eid, reqMsg,
+                        respMsg));
+}
+
+void MctpRequester::sendRecvMsg(
+    uint8_t eid, const std::span<const uint8_t> reqMsg,
+    std::span<uint8_t> respMsg, std::move_only_function<void(int)> callback)
+{
+    if (reqMsg.size() < sizeof(ocp::accelerator_management::BindingPciVid))
+    {
+        lg2::error("MctpRequester: Message too small");
+        callback(EPROTO);
+        return;
+    }
+
+    completionCallback = std::move(callback);
+
+    struct sockaddr_mctp addr{};
+    addr.smctp_family = AF_MCTP;
+    addr.smctp_addr.s_addr = eid;
+    addr.smctp_type = msgType;
+    addr.smctp_tag = MCTP_TAG_OWNER;
+
+    sendEndPoint = {&addr, sizeof(addr)};
+
+    mctpSocket.async_send_to(
+        boost::asio::const_buffer(reqMsg), sendEndPoint,
+        std::bind_front(&MctpRequester::handleSendMsgCompletion, this, eid,
+                        reqMsg, respMsg));
+}
+} // namespace mctp
diff --git a/src/nvidia-gpu/MctpRequester.hpp b/src/nvidia-gpu/MctpRequester.hpp
new file mode 100644
index 0000000..289e800
--- /dev/null
+++ b/src/nvidia-gpu/MctpRequester.hpp
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <OcpMctpVdm.hpp>
+#include <boost/asio/generic/datagram_protocol.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/steady_timer.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <span>
+
+namespace mctp
+{
+class MctpRequester
+{
+  public:
+    MctpRequester() = delete;
+
+    MctpRequester(const MctpRequester&) = delete;
+
+    MctpRequester(MctpRequester&&) = delete;
+
+    MctpRequester& operator=(const MctpRequester&) = delete;
+
+    MctpRequester& operator=(MctpRequester&&) = delete;
+
+    explicit MctpRequester(boost::asio::io_context& ctx);
+
+    void sendRecvMsg(uint8_t eid, std::span<const uint8_t> reqMsg,
+                     std::span<uint8_t> respMsg,
+                     std::move_only_function<void(int)> callback);
+
+  private:
+    void processRecvMsg(uint8_t eid, std::span<const uint8_t> reqMsg,
+                        std::span<uint8_t> respMsg,
+                        const boost::system::error_code& ec, size_t length);
+
+    void handleSendMsgCompletion(uint8_t eid, std::span<const uint8_t> reqMsg,
+                                 std::span<uint8_t> respMsg,
+                                 const boost::system::error_code& ec,
+                                 size_t length);
+
+    boost::asio::generic::datagram_protocol::socket mctpSocket;
+
+    static constexpr size_t maxMessageSize = 65536 + 256;
+
+    boost::asio::generic::datagram_protocol::endpoint sendEndPoint;
+
+    boost::asio::generic::datagram_protocol::endpoint recvEndPoint;
+
+    boost::asio::steady_timer expiryTimer;
+
+    std::move_only_function<void(int)> completionCallback;
+
+    static constexpr uint8_t msgType = ocp::accelerator_management::messageType;
+};
+} // namespace mctp
diff --git a/src/nvidia-gpu/NvidiaGpuMctpVdm.cpp b/src/nvidia-gpu/NvidiaGpuMctpVdm.cpp
new file mode 100644
index 0000000..17f71e0
--- /dev/null
+++ b/src/nvidia-gpu/NvidiaGpuMctpVdm.cpp
@@ -0,0 +1,156 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "NvidiaGpuMctpVdm.hpp"
+
+#include "OcpMctpVdm.hpp"
+
+#include <endian.h>
+
+#include <cerrno>
+#include <cstdint>
+#include <cstring>
+#include <span>
+
+namespace gpu
+{
+// These functions encode/decode data communicated over the network
+// The use of reinterpret_cast enables direct memory access to raw byte buffers
+// without doing unnecessary data copying
+// NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)
+int packHeader(const ocp::accelerator_management::BindingPciVidInfo& hdr,
+               ocp::accelerator_management::BindingPciVid& msg)
+{
+    return ocp::accelerator_management::packHeader(nvidiaPciVendorId, hdr, msg);
+}
+
+int encodeQueryDeviceIdentificationRequest(uint8_t instanceId,
+                                           const std::span<uint8_t> buf)
+{
+    if (buf.size() < sizeof(QueryDeviceIdentificationRequest))
+    {
+        return EINVAL;
+    }
+
+    auto* msg = reinterpret_cast<QueryDeviceIdentificationRequest*>(buf.data());
+
+    ocp::accelerator_management::BindingPciVidInfo header{};
+
+    header.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    header.instance_id = instanceId &
+                         ocp::accelerator_management::instanceIdBitMask;
+    header.msg_type =
+        static_cast<uint8_t>(MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    auto rc = packHeader(header, msg->hdr.msgHdr.hdr);
+
+    if (rc != 0)
+    {
+        return rc;
+    }
+
+    msg->hdr.command = static_cast<uint8_t>(
+        DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    msg->hdr.data_size = 0;
+
+    return 0;
+}
+
+int decodeQueryDeviceIdentificationResponse(
+    const std::span<const uint8_t> buf,
+    ocp::accelerator_management::CompletionCode& cc, uint16_t& reasonCode,
+    uint8_t& deviceIdentification, uint8_t& deviceInstance)
+{
+    auto rc =
+        ocp::accelerator_management::decodeReasonCodeAndCC(buf, cc, reasonCode);
+
+    if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    if (buf.size() < sizeof(QueryDeviceIdentificationResponse))
+    {
+        return EINVAL;
+    }
+
+    const auto* response =
+        reinterpret_cast<const QueryDeviceIdentificationResponse*>(buf.data());
+
+    deviceIdentification = response->device_identification;
+    deviceInstance = response->instance_id;
+
+    return 0;
+}
+
+int encodeGetTemperatureReadingRequest(uint8_t instanceId, uint8_t sensorId,
+                                       std::span<uint8_t> buf)
+{
+    if (buf.size() < sizeof(GetTemperatureReadingRequest))
+    {
+        return EINVAL;
+    }
+
+    auto* msg = reinterpret_cast<GetTemperatureReadingRequest*>(buf.data());
+
+    ocp::accelerator_management::BindingPciVidInfo header{};
+    header.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    header.instance_id = instanceId &
+                         ocp::accelerator_management::instanceIdBitMask;
+    header.msg_type = static_cast<uint8_t>(MessageType::PLATFORM_ENVIRONMENTAL);
+
+    auto rc = packHeader(header, msg->hdr.msgHdr.hdr);
+
+    if (rc != 0)
+    {
+        return rc;
+    }
+
+    msg->hdr.command = static_cast<uint8_t>(
+        PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    msg->hdr.data_size = sizeof(sensorId);
+    msg->sensor_id = sensorId;
+
+    return 0;
+}
+
+int decodeGetTemperatureReadingResponse(
+    const std::span<const uint8_t> buf,
+    ocp::accelerator_management::CompletionCode& cc, uint16_t& reasonCode,
+    double& temperatureReading)
+{
+    auto rc =
+        ocp::accelerator_management::decodeReasonCodeAndCC(buf, cc, reasonCode);
+
+    if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    if (buf.size() < sizeof(GetTemperatureReadingResponse))
+    {
+        return EINVAL;
+    }
+
+    const auto* response =
+        reinterpret_cast<const GetTemperatureReadingResponse*>(buf.data());
+
+    uint16_t dataSize = le16toh(response->hdr.data_size);
+
+    if (dataSize != sizeof(int32_t))
+    {
+        return EINVAL;
+    }
+
+    int32_t reading = le32toh(response->reading);
+    temperatureReading = reading / static_cast<double>(1 << 8);
+
+    return 0;
+}
+// NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
+} // namespace gpu
diff --git a/src/nvidia-gpu/NvidiaGpuMctpVdm.hpp b/src/nvidia-gpu/NvidiaGpuMctpVdm.hpp
new file mode 100644
index 0000000..ce3b393
--- /dev/null
+++ b/src/nvidia-gpu/NvidiaGpuMctpVdm.hpp
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <OcpMctpVdm.hpp>
+
+#include <cstdint>
+#include <span>
+
+namespace gpu
+{
+
+constexpr uint16_t nvidiaPciVendorId = 0x10de;
+
+enum class MessageType : uint8_t
+{
+    DEVICE_CAPABILITY_DISCOVERY = 0,
+    PLATFORM_ENVIRONMENTAL = 3
+};
+
+enum class DeviceCapabilityDiscoveryCommands : uint8_t
+{
+    QUERY_DEVICE_IDENTIFICATION = 0x09,
+};
+
+enum class PlatformEnvironmentalCommands : uint8_t
+{
+    GET_TEMPERATURE_READING = 0x00,
+};
+
+enum class DeviceIdentification : uint8_t
+{
+    DEVICE_GPU = 0
+};
+
+struct QueryDeviceIdentificationRequest
+{
+    ocp::accelerator_management::CommonRequest hdr;
+} __attribute__((packed));
+
+struct QueryDeviceIdentificationResponse
+{
+    ocp::accelerator_management::CommonResponse hdr;
+    uint8_t device_identification;
+    uint8_t instance_id;
+} __attribute__((packed));
+
+struct GetNumericSensorReadingRequest
+{
+    ocp::accelerator_management::CommonRequest hdr;
+    uint8_t sensor_id;
+} __attribute__((packed));
+
+using GetTemperatureReadingRequest = GetNumericSensorReadingRequest;
+
+struct GetTemperatureReadingResponse
+{
+    ocp::accelerator_management::CommonResponse hdr;
+    int32_t reading;
+} __attribute__((packed));
+
+int packHeader(const ocp::accelerator_management::BindingPciVidInfo& hdr,
+               ocp::accelerator_management::BindingPciVid& msg);
+
+int encodeQueryDeviceIdentificationRequest(uint8_t instanceId,
+                                           std::span<uint8_t> buf);
+
+int decodeQueryDeviceIdentificationResponse(
+    std::span<const uint8_t> buf,
+    ocp::accelerator_management::CompletionCode& cc, uint16_t& reasonCode,
+    uint8_t& deviceIdentification, uint8_t& deviceInstance);
+
+int encodeGetTemperatureReadingRequest(uint8_t instanceId, uint8_t sensorId,
+                                       std::span<uint8_t> buf);
+
+int decodeGetTemperatureReadingResponse(
+    std::span<const uint8_t> buf,
+    ocp::accelerator_management::CompletionCode& cc, uint16_t& reasonCode,
+    double& temperatureReading);
+
+} // namespace gpu
diff --git a/src/nvidia-gpu/NvidiaGpuSensor.cpp b/src/nvidia-gpu/NvidiaGpuSensor.cpp
index 3594c29..86b356b 100644
--- a/src/nvidia-gpu/NvidiaGpuSensor.cpp
+++ b/src/nvidia-gpu/NvidiaGpuSensor.cpp
@@ -1,16 +1,21 @@
 /*
  * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
- * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
 
 #include "NvidiaGpuSensor.hpp"
 
+#include "SensorPaths.hpp"
 #include "Thresholds.hpp"
 #include "Utils.hpp"
 #include "sensor.hpp"
 
 #include <bits/basic_string.h>
 
+#include <MctpRequester.hpp>
+#include <NvidiaGpuMctpVdm.hpp>
+#include <OcpMctpVdm.hpp>
 #include <boost/asio/io_context.hpp>
 #include <boost/container/flat_map.hpp>
 #include <phosphor-logging/lg2.hpp>
@@ -23,6 +28,7 @@
 #include <chrono>
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <memory>
 #include <string>
 #include <utility>
@@ -31,20 +37,23 @@
 
 using namespace std::literals;
 
+constexpr uint8_t gpuTempSensorId{0};
 static constexpr double gpuTempSensorMaxReading = 127;
 static constexpr double gpuTempSensorMinReading = -128;
 
 GpuTempSensor::GpuTempSensor(
     std::shared_ptr<sdbusplus::asio::connection>& conn,
-    boost::asio::io_context& io, const std::string& name,
-    const std::string& sensorConfiguration,
+    boost::asio::io_context& io, mctp::MctpRequester& mctpRequester,
+    const std::string& name, const std::string& sensorConfiguration,
     sdbusplus::asio::object_server& objectServer,
-    std::vector<thresholds::Threshold>&& thresholdData) :
+    std::vector<thresholds::Threshold>&& thresholdData,
+    std::chrono::milliseconds pollRate) :
     Sensor(escapeName(name), std::move(thresholdData), sensorConfiguration,
            "temperature", false, true, gpuTempSensorMaxReading,
            gpuTempSensorMinReading, conn),
-    waitTimer(io, std::chrono::steady_clock::duration(0)), conn(conn),
-    objectServer(objectServer)
+    sensorId{gpuTempSensorId}, sensorPollMs(pollRate),
+    waitTimer(io, std::chrono::steady_clock::duration(0)),
+    mctpRequester(mctpRequester), conn(conn), objectServer(objectServer)
 {
     std::string dbusPath =
         sensorPathPrefix + "temperature/"s + escapeName(name);
@@ -115,6 +124,129 @@
     }
 }
 
+void GpuTempSensor::read()
+{
+    update();
+
+    waitTimer.expires_after(std::chrono::milliseconds(sensorPollMs));
+    waitTimer.async_wait(
+        [weakPtrToThis = std::weak_ptr<GpuTempSensor>{shared_from_this()}](
+            const boost::system::error_code& ec) {
+            if (ec)
+            {
+                return;
+            }
+            if (auto ptr = weakPtrToThis.lock())
+            {
+                ptr->read();
+            }
+        });
+}
+
+void GpuTempSensor::processResponse(int sendRecvMsgResult)
+{
+    if (sendRecvMsgResult != 0)
+    {
+        lg2::error(
+            "Error updating Temperature Sensor: sending message over MCTP failed, rc={RC}",
+            "RC", sendRecvMsgResult);
+        return;
+    }
+
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode = 0;
+    double tempValue = 0;
+
+    auto rc = gpu::decodeGetTemperatureReadingResponse(
+        getTemperatureReadingResponse, cc, reasonCode, tempValue);
+
+    if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        lg2::error(
+            "Error updating Temperature Sensor: decode failed, rc={RC}, cc={CC}, reasonCode={RESC}",
+            "RC", rc, "CC", cc, "RESC", reasonCode);
+        return;
+    }
+
+    updateValue(tempValue);
+}
+
+void GpuTempSensor::update()
+{
+    auto rc = gpu::encodeGetTemperatureReadingRequest(
+        0, sensorId, getTemperatureReadingRequest);
+    if (rc != 0)
+    {
+        lg2::error("Error updating Temperature Sensor: encode failed, rc={RC}",
+                   "RC", rc);
+        return;
+    }
+
+    mctpRequester.sendRecvMsg(
+        eid, getTemperatureReadingRequest, getTemperatureReadingResponse,
+        [this](int sendRecvMsgResult) { processResponse(sendRecvMsgResult); });
+}
+
+void GpuTempSensor::processQueryDeviceIdResponse(uint8_t eid,
+                                                 int sendRecvMsgResult)
+{
+    if (sendRecvMsgResult != 0)
+    {
+        lg2::error(
+            "Error processing GPU endpoint: sending message over MCTP failed, rc={RC}",
+            "RC", sendRecvMsgResult);
+        return;
+    }
+
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode = 0;
+    uint8_t responseDeviceType = 0;
+    uint8_t responseInstanceId = 0;
+
+    auto rc = gpu::decodeQueryDeviceIdentificationResponse(
+        queryDeviceIdentificationResponse, cc, reasonCode, responseDeviceType,
+        responseInstanceId);
+
+    if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        lg2::error(
+            "Error processing GPU endpoint: decode failed, rc={RC}, cc={CC}, reasonCode={RESC}",
+            "RC", rc, "CC", cc, "RESC", reasonCode);
+        return;
+    }
+
+    if (responseDeviceType ==
+        static_cast<uint8_t>(gpu::DeviceIdentification::DEVICE_GPU))
+    {
+        lg2::info(
+            "Found the GPU with EID {EID}, DeviceType {DEVTYPE}, InstanceId {IID}.",
+            "EID", eid, "DEVTYPE", responseDeviceType, "IID",
+            responseInstanceId);
+
+        this->eid = eid;
+        setInitialProperties(sensor_paths::unitDegreesC);
+        read();
+    }
+}
+
+void GpuTempSensor::processGpuEndpoint(uint8_t eid)
+{
+    auto rc = gpu::encodeQueryDeviceIdentificationRequest(
+        0, queryDeviceIdentificationRequest);
+    if (rc != 0)
+    {
+        lg2::error("Error processing GPU endpoint: encode failed, rc={RC}",
+                   "RC", rc);
+        return;
+    }
+
+    mctpRequester.sendRecvMsg(
+        eid, queryDeviceIdentificationRequest,
+        queryDeviceIdentificationResponse, [this, eid](int sendRecvMsgResult) {
+            processQueryDeviceIdResponse(eid, sendRecvMsgResult);
+        });
+}
+
 void GpuTempSensor::processEndpoint(const boost::system::error_code& ec,
                                     const SensorBaseConfigMap& endpoint)
 {
@@ -125,7 +257,7 @@
         return;
     }
 
-    [[maybe_unused]] uint8_t eid{};
+    uint8_t eid{};
     std::vector<uint8_t> mctpTypes{};
 
     auto hasEid = endpoint.find("EID");
@@ -173,9 +305,14 @@
         return;
     }
 
-    // if the OCP MCTP VDM Message type (0x7E) is found in mctpTypes
-    // process the endpoint further.
-    (void)this;
+    if (std::find(mctpTypes.begin(), mctpTypes.end(),
+                  ocp::accelerator_management::messageType) != mctpTypes.end())
+    {
+        lg2::info(
+            "GpuTempSensor::discoverGpus(): Found OCP MCTP VDM Endpoint with ID {EID}",
+            "EID", eid);
+        this->processGpuEndpoint(eid);
+    }
 }
 
 void GpuTempSensor::discoverGpus()
@@ -198,7 +335,7 @@
     boost::container::flat_map<std::string, std::shared_ptr<GpuTempSensor>>&
         sensors,
     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
-    const ManagedObjectType& resp)
+    mctp::MctpRequester& mctpRequester, const ManagedObjectType& resp)
 {
     for (const auto& [path, interfaces] : resp)
     {
@@ -211,9 +348,12 @@
 
             std::string name = loadVariant<std::string>(cfg, "Name");
 
+            uint64_t pollRate = loadVariant<uint64_t>(cfg, "PollRate");
+
             sensors[name] = std::make_shared<GpuTempSensor>(
-                dbusConnection, io, name, path, objectServer,
-                std::vector<thresholds::Threshold>{});
+                dbusConnection, io, mctpRequester, name, path, objectServer,
+                std::vector<thresholds::Threshold>{},
+                std::chrono::milliseconds{pollRate});
 
             lg2::info(
                 "Added GPU Temperature Sensor {NAME} with chassis path: {PATH}.",
@@ -226,7 +366,8 @@
     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
     boost::container::flat_map<std::string, std::shared_ptr<GpuTempSensor>>&
         sensors,
-    std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
+    std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
+    mctp::MctpRequester& mctpRequester)
 {
     if (!dbusConnection)
     {
@@ -234,7 +375,7 @@
         return;
     }
     dbusConnection->async_method_call(
-        [&sensors, &dbusConnection, &io,
+        [&sensors, &mctpRequester, &dbusConnection, &io,
          &objectServer](const boost::system::error_code& ec,
                         const ManagedObjectType& resp) {
             if (ec)
@@ -244,7 +385,7 @@
             }
 
             processSensorConfigs(io, objectServer, sensors, dbusConnection,
-                                 resp);
+                                 mctpRequester, resp);
         },
         entityManagerName, "/xyz/openbmc_project/inventory",
         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
diff --git a/src/nvidia-gpu/NvidiaGpuSensor.hpp b/src/nvidia-gpu/NvidiaGpuSensor.hpp
index 14627fc..158dc41 100644
--- a/src/nvidia-gpu/NvidiaGpuSensor.hpp
+++ b/src/nvidia-gpu/NvidiaGpuSensor.hpp
@@ -1,14 +1,18 @@
 /*
  * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
- * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
 
 #pragma once
 
+#include "MctpRequester.hpp"
 #include "Thresholds.hpp"
 #include "Utils.hpp"
 #include "sensor.hpp"
 
+#include <NvidiaGpuMctpVdm.hpp>
+#include <OcpMctpVdm.hpp>
 #include <boost/asio/io_context.hpp>
 #include <boost/asio/steady_timer.hpp>
 #include <boost/container/flat_map.hpp>
@@ -16,6 +20,9 @@
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/message.hpp>
 
+#include <array>
+#include <chrono>
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <vector>
@@ -28,95 +35,74 @@
     public std::enable_shared_from_this<GpuTempSensor>
 {
   public:
-    /**
-     * @brief Constructor for GpuTempSensor
-     * @param conn D-Bus connection
-     * @param io Boost ASIO I/O context for asynchronous operations
-     * @param mctpRequester MCTP protocol requester for GPU communication
-     * @param name Name of the sensor
-     * @param sensorConfiguration Configuration string for the sensor
-     * @param objectServer D-Bus object server
-     * @param thresholdData Vector of threshold configurations
-     * @param pollRate How often to poll for new readings
-     * @param deviceInfo Information about the GPU device
-     * @param verbose Whether to enable verbose logging
-     */
     GpuTempSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
-                  boost::asio::io_context& io, const std::string& name,
+                  boost::asio::io_context& io,
+                  mctp::MctpRequester& mctpRequester, const std::string& name,
                   const std::string& sensorConfiguration,
                   sdbusplus::asio::object_server& objectServer,
-                  std::vector<thresholds::Threshold>&& thresholdData);
+                  std::vector<thresholds::Threshold>&& thresholdData,
+                  std::chrono::milliseconds pollRate);
 
-    /**
-     * @brief Destructor
-     */
     ~GpuTempSensor() override;
 
-    /**
-     * @brief Check if any thresholds have been crossed
-     * @details Overrides the base class method to implement GPU-specific
-     * threshold checking
-     */
     void checkThresholds() override;
 
   private:
-    /**
-     * @brief Discover available GPUs on the system
-     */
+    void read();
+
+    void update();
+
     void discoverGpus();
 
-    /**
-     * @brief Process MCTP endpoints discovered on the system
-     *
-     * @param[in] ec Error code from the D-Bus method call
-     * @param[in] ret Object tree results containing MCTP endpoint information
-     */
+    void processResponse(int sendRecvMsgResult);
+
+    void processQueryDeviceIdResponse(uint8_t eid, int sendRecvMsgResult);
+
     void queryEndpoints(const boost::system::error_code& ec,
                         const GetSubTreeType& ret);
 
-    /**
-     * @brief Process configuration properties for MCTP endpoints
-     *
-     * @param[in] ec Error code from the D-Bus properties method call
-     * @param[in] configs Map of configuration properties for the endpoint
-     */
     void processEndpoint(const boost::system::error_code& ec,
                          const SensorBaseConfigMap& endpoint);
+    void processGpuEndpoint(uint8_t eid);
 
-    /**
-     * @brief Timer for scheduling sensor reads
-     */
+    uint8_t eid{};
+
+    uint8_t sensorId;
+
+    std::chrono::milliseconds sensorPollMs;
+
     boost::asio::steady_timer waitTimer;
 
-    /**
-     * @brief D-Bus connection
-     */
+    mctp::MctpRequester& mctpRequester;
+
     std::shared_ptr<sdbusplus::asio::connection> conn;
 
-    /**
-     * @brief D-Bus object server
-     */
     sdbusplus::asio::object_server& objectServer;
+
+    std::array<uint8_t, sizeof(ocp::accelerator_management::Message) +
+                            sizeof(gpu::GetTemperatureReadingRequest)>
+        getTemperatureReadingRequest{};
+
+    std::array<uint8_t, sizeof(ocp::accelerator_management::Message) +
+                            sizeof(gpu::GetTemperatureReadingResponse)>
+        getTemperatureReadingResponse{};
+
+    std::array<uint8_t, sizeof(ocp::accelerator_management::Message) +
+                            sizeof(gpu::QueryDeviceIdentificationRequest)>
+        queryDeviceIdentificationRequest{};
+
+    std::array<uint8_t, sizeof(ocp::accelerator_management::Message) +
+                            sizeof(gpu::QueryDeviceIdentificationResponse)>
+        queryDeviceIdentificationResponse{};
 };
 
-/**
- * @brief Create GPU temperature sensors
- * @param io Boost ASIO I/O context
- * @param objectServer D-Bus object server
- * @param sensors Map to store created sensors
- * @param dbusConnection D-Bus connection
- */
 void createSensors(
     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
     boost::container::flat_map<std::string, std::shared_ptr<GpuTempSensor>>&
         sensors,
-    std::shared_ptr<sdbusplus::asio::connection>& dbusConnection);
+    std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
+    mctp::MctpRequester& mctpRequester);
 
-/**
- * @brief Handle D-Bus interface removal events
- * @param message D-Bus message containing interface removal information
- * @param sensors Map of GPU temperature sensors to check for removal
- */
 void interfaceRemoved(
     sdbusplus::message_t& message,
     boost::container::flat_map<std::string, std::shared_ptr<GpuTempSensor>>&
diff --git a/src/nvidia-gpu/NvidiaGpuSensorMain.cpp b/src/nvidia-gpu/NvidiaGpuSensorMain.cpp
index 9879c2c..cf764d3 100644
--- a/src/nvidia-gpu/NvidiaGpuSensorMain.cpp
+++ b/src/nvidia-gpu/NvidiaGpuSensorMain.cpp
@@ -1,8 +1,10 @@
 /*
  * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
- * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
 
+#include "MctpRequester.hpp"
 #include "NvidiaGpuSensor.hpp"
 #include "Utils.hpp"
 
@@ -30,13 +32,13 @@
 void configTimerExpiryCallback(
     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
-    const boost::system::error_code& ec)
+    mctp::MctpRequester& mctpRequester, const boost::system::error_code& ec)
 {
     if (ec == boost::asio::error::operation_aborted)
     {
         return; // we're being canceled
     }
-    createSensors(io, objectServer, sensors, dbusConnection);
+    createSensors(io, objectServer, sensors, dbusConnection, mctpRequester);
     if (sensors.empty())
     {
         lg2::info("Configuration not detected");
@@ -51,19 +53,22 @@
     objectServer.add_manager("/xyz/openbmc_project/sensors");
     systemBus->request_name("xyz.openbmc_project.GpuSensor");
 
+    mctp::MctpRequester mctpRequester(io);
+
     boost::asio::post(io, [&]() {
-        createSensors(io, objectServer, sensors, systemBus);
+        createSensors(io, objectServer, sensors, systemBus, mctpRequester);
     });
 
     boost::asio::steady_timer configTimer(io);
 
     std::function<void(sdbusplus::message_t&)> eventHandler =
-        [&configTimer, &io, &objectServer, &systemBus](sdbusplus::message_t&) {
+        [&configTimer, &io, &objectServer, &systemBus,
+         &mctpRequester](sdbusplus::message_t&) {
             configTimer.expires_after(std::chrono::seconds(1));
             // create a timer because normally multiple properties change
-            configTimer.async_wait(
-                std::bind_front(configTimerExpiryCallback, std::ref(io),
-                                std::ref(objectServer), std::ref(systemBus)));
+            configTimer.async_wait(std::bind_front(
+                configTimerExpiryCallback, std::ref(io), std::ref(objectServer),
+                std::ref(systemBus), std::ref(mctpRequester)));
         };
 
     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
diff --git a/src/nvidia-gpu/OcpMctpVdm.cpp b/src/nvidia-gpu/OcpMctpVdm.cpp
new file mode 100644
index 0000000..6497621
--- /dev/null
+++ b/src/nvidia-gpu/OcpMctpVdm.cpp
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "OcpMctpVdm.hpp"
+
+#include <endian.h>
+
+#include <cerrno>
+#include <cstdint>
+#include <cstring>
+#include <span>
+
+namespace ocp
+{
+namespace accelerator_management
+{
+
+int packHeader(uint16_t pciVendorId, const BindingPciVidInfo& hdr,
+               BindingPciVid& msg)
+{
+    if (hdr.ocp_accelerator_management_msg_type !=
+            static_cast<uint8_t>(MessageType::RESPONSE) &&
+        hdr.ocp_accelerator_management_msg_type !=
+            static_cast<uint8_t>(MessageType::REQUEST))
+    {
+        return EINVAL;
+    }
+
+    if (hdr.instance_id > instanceMax)
+    {
+        return EINVAL;
+    }
+
+    msg.instance_id = hdr.instance_id & instanceIdBitMask;
+
+    if (hdr.ocp_accelerator_management_msg_type ==
+        static_cast<uint8_t>(MessageType::REQUEST))
+    {
+        msg.instance_id |= requestBitMask;
+    }
+    else
+    {
+        msg.instance_id &= ~requestBitMask;
+    }
+
+    msg.pci_vendor_id = htobe16(pciVendorId);
+    msg.instance_id &= ~instanceIdReservedBitMask;
+    msg.ocp_version = ocpVersion & ocpVersionBitMask;
+    msg.ocp_version |= (ocpType << ocpTypeBitOffset) & ocpTypeBitMask;
+    msg.ocp_accelerator_management_msg_type = hdr.msg_type;
+
+    return 0;
+}
+
+int decodeReasonCodeAndCC(const std::span<const uint8_t> buf,
+                          CompletionCode& cc, uint16_t& reasonCode)
+{
+    if (buf.size() <
+        sizeof(ocp::accelerator_management::CommonNonSuccessResponse))
+    {
+        return EINVAL;
+    }
+
+    // These expression decodes data communicated over the network
+    // The use of reinterpret_cast enables direct memory access to raw byte
+    // buffers without doing unnecessary data copying
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+    const auto* response = reinterpret_cast<
+        const ocp::accelerator_management::CommonNonSuccessResponse*>(
+        buf.data());
+
+    cc = static_cast<CompletionCode>(response->completion_code);
+    if (cc == CompletionCode::SUCCESS)
+    {
+        reasonCode = 0;
+        return 0;
+    }
+
+    // reason code is expected to be present if CC != SUCCESS
+    reasonCode = le16toh(response->reason_code);
+
+    return 0;
+}
+} // namespace accelerator_management
+} // namespace ocp
diff --git a/src/nvidia-gpu/OcpMctpVdm.hpp b/src/nvidia-gpu/OcpMctpVdm.hpp
new file mode 100644
index 0000000..735d0a5
--- /dev/null
+++ b/src/nvidia-gpu/OcpMctpVdm.hpp
@@ -0,0 +1,107 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <span>
+
+namespace ocp
+{
+namespace accelerator_management
+{
+
+constexpr uint8_t messageType = 0x7E;
+
+constexpr uint8_t ocpType = 8;
+constexpr uint8_t ocpVersion = 9;
+constexpr uint8_t ocpTypeBitOffset = 4;
+constexpr uint8_t ocpTypeBitMask = 0b11110000;
+constexpr uint8_t ocpVersionBitMask = 0b00001111;
+constexpr uint8_t instanceIdBitMask = 0b00011111;
+constexpr uint8_t instanceIdReservedBitMask = 0b00100000;
+constexpr uint8_t datagramBitMask = 0b01000000;
+constexpr uint8_t requestBitMask = 0b10000000;
+
+constexpr uint8_t instanceMin = 0;
+constexpr uint8_t instanceMax = 31;
+
+enum class CompletionCode : uint8_t
+{
+    SUCCESS = 0x00,
+    ERROR = 0x01,
+    ERR_INVALID_DATA = 0x02,
+    ERR_INVALID_DATA_LENGTH = 0x03,
+    ERR_NOT_READY = 0x04,
+    ERR_UNSUPPORTED_COMMAND_CODE = 0x05,
+    ERR_UNSUPPORTED_MSG_TYPE = 0x06,
+    ERR_BUS_ACCESS = 0x7f,
+    ERR_NULL = 0x80,
+};
+
+enum class ReasonCode : uint16_t
+{
+    REASON_NONE = 0x00,
+};
+
+enum class MessageType : uint8_t
+{
+    RESPONSE = 0, //!< OCP MCTP VDM response message
+    REQUEST = 2,  //!< OCP MCTP VDM request message
+};
+
+struct BindingPciVid
+{
+    uint16_t pci_vendor_id;                      //!< PCI defined vendor ID
+    uint8_t instance_id;                         //!< Instance ID
+    uint8_t ocp_version;                         //!< OCP version
+    uint8_t ocp_accelerator_management_msg_type; //!< Message Type
+} __attribute__((packed));
+
+struct Message
+{
+    BindingPciVid hdr; //!< OCP MCTP VDM message header
+} __attribute__((packed));
+
+struct BindingPciVidInfo
+{
+    uint8_t ocp_accelerator_management_msg_type;
+    uint8_t instance_id;
+    uint8_t msg_type;
+};
+
+struct CommonRequest
+{
+    Message msgHdr;
+    uint8_t command;
+    uint8_t data_size;
+} __attribute__((packed));
+
+struct CommonResponse
+{
+    Message msgHdr;
+    uint8_t command;
+    uint8_t completion_code;
+    uint16_t reserved;
+    uint16_t data_size;
+} __attribute__((packed));
+
+struct CommonNonSuccessResponse
+{
+    Message msgHdr;
+    uint8_t command;
+    uint8_t completion_code;
+    uint16_t reason_code;
+} __attribute__((packed));
+
+int packHeader(uint16_t pciVendorId, const BindingPciVidInfo& hdr,
+               BindingPciVid& msg);
+
+int decodeReasonCodeAndCC(std::span<const uint8_t> buf, CompletionCode& cc,
+                          uint16_t& reasonCode);
+
+} // namespace accelerator_management
+} // namespace ocp
diff --git a/src/nvidia-gpu/meson.build b/src/nvidia-gpu/meson.build
index 9918435..97b6da5 100644
--- a/src/nvidia-gpu/meson.build
+++ b/src/nvidia-gpu/meson.build
@@ -1,4 +1,10 @@
-gpusensor_sources = files('NvidiaGpuSensor.cpp', 'NvidiaGpuSensorMain.cpp')
+gpusensor_sources = files(
+    'MctpRequester.cpp',
+    'NvidiaGpuMctpVdm.cpp',
+    'NvidiaGpuSensor.cpp',
+    'NvidiaGpuSensorMain.cpp',
+    'OcpMctpVdm.cpp',
+)
 
 gpusensor_include_dir = include_directories('.', is_system: true)
 sensor_include_dir = include_directories('../..')
@@ -11,3 +17,7 @@
     dependencies: [thresholds_dep, utils_dep],
     install: true,
 )
+
+if get_option('tests').enabled()
+    subdir('tests')
+endif
diff --git a/src/nvidia-gpu/tests/NvidiaGpuSensorTest.cpp b/src/nvidia-gpu/tests/NvidiaGpuSensorTest.cpp
new file mode 100644
index 0000000..c630ffa
--- /dev/null
+++ b/src/nvidia-gpu/tests/NvidiaGpuSensorTest.cpp
@@ -0,0 +1,500 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "NvidiaGpuMctpVdm.hpp"
+#include "OcpMctpVdm.hpp"
+
+#include <endian.h>
+
+#include <array>
+#include <cerrno>
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ocp_mctp_tests
+{
+
+class OcpMctpVdmTests : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        // Initialize common test data here
+    }
+};
+
+// Tests for OcpMctpVdm::packHeader function
+TEST_F(OcpMctpVdmTests, PackHeaderRequestSuccess)
+{
+    const uint16_t pciVendorId = 0x1234;
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    hdr.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    hdr.instance_id = 5;
+    hdr.msg_type = 0x7E;
+
+    int result = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(msg.pci_vendor_id, htobe16(pciVendorId));
+    EXPECT_EQ(msg.instance_id & ocp::accelerator_management::instanceIdBitMask,
+              5);
+    EXPECT_NE(msg.instance_id & ocp::accelerator_management::requestBitMask, 0);
+    EXPECT_EQ(msg.ocp_version & 0x0F, ocp::accelerator_management::ocpVersion);
+    EXPECT_EQ((msg.ocp_version & 0xF0) >>
+                  ocp::accelerator_management::ocpTypeBitOffset,
+              ocp::accelerator_management::ocpType);
+    EXPECT_EQ(msg.ocp_accelerator_management_msg_type, 0x7E);
+}
+
+TEST_F(OcpMctpVdmTests, PackHeaderResponseSuccess)
+{
+    const uint16_t pciVendorId = 0x1234;
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    hdr.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    hdr.instance_id = 10;
+    hdr.msg_type = 0x7E;
+
+    int result = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(msg.pci_vendor_id, htobe16(pciVendorId));
+    EXPECT_EQ(msg.instance_id & ocp::accelerator_management::instanceIdBitMask,
+              10);
+    EXPECT_EQ(msg.instance_id & ocp::accelerator_management::requestBitMask, 0);
+    EXPECT_EQ(msg.ocp_version & 0x0F, ocp::accelerator_management::ocpVersion);
+    EXPECT_EQ((msg.ocp_version & 0xF0) >>
+                  ocp::accelerator_management::ocpTypeBitOffset,
+              ocp::accelerator_management::ocpType);
+    EXPECT_EQ(msg.ocp_accelerator_management_msg_type, 0x7E);
+}
+
+TEST_F(OcpMctpVdmTests, PackHeaderInvalidMessageType)
+{
+    const uint16_t pciVendorId = 0x1234;
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    hdr.ocp_accelerator_management_msg_type = 3; // Invalid message type
+    hdr.instance_id = 5;
+    hdr.msg_type = 0x7E;
+
+    int result = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+
+    EXPECT_EQ(result, EINVAL);
+}
+
+TEST_F(OcpMctpVdmTests, PackHeaderInvalidInstanceId)
+{
+    const uint16_t pciVendorId = 0x1234;
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    hdr.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    hdr.instance_id = 32; // Out of range (0-31 valid)
+    hdr.msg_type = 0x7E;
+
+    int result = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+
+    EXPECT_EQ(result, EINVAL);
+}
+
+// Tests for OcpMctpVdm::decodeReasonCodeAndCC function
+TEST_F(OcpMctpVdmTests, DecodeReasonCodeAndCCSuccessCase)
+{
+    ocp::accelerator_management::CommonNonSuccessResponse response{};
+    response.command = 0x42;
+    response.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    response.reason_code = htole16(0x1234);
+
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+
+    std::array<uint8_t, sizeof(response)> buf{};
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    int result =
+        ocp::accelerator_management::decodeReasonCodeAndCC(buf, cc, reasonCode);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(reasonCode, 0); // Should be 0 for SUCCESS
+}
+
+TEST_F(OcpMctpVdmTests, DecodeReasonCodeAndCCErrorCase)
+{
+    ocp::accelerator_management::CommonNonSuccessResponse response{};
+    response.command = 0x42;
+    response.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    response.reason_code = htole16(0x5678);
+
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+
+    std::array<uint8_t, sizeof(response)> buf{};
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    int result =
+        ocp::accelerator_management::decodeReasonCodeAndCC(buf, cc, reasonCode);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::ERROR);
+    EXPECT_EQ(reasonCode, 0x5678);
+}
+
+} // namespace ocp_mctp_tests
+
+namespace gpu_mctp_tests
+{
+
+class GpuMctpVdmTests : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        // Initialize common test data here
+    }
+};
+
+// Tests for GpuMctpVdm::packHeader function
+TEST_F(GpuMctpVdmTests, PackHeaderSuccess)
+{
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    hdr.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    hdr.instance_id = 5;
+    hdr.msg_type = 0x7E;
+
+    int result = gpu::packHeader(hdr, msg);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(msg.pci_vendor_id, htobe16(gpu::nvidiaPciVendorId));
+    EXPECT_EQ(msg.instance_id & ocp::accelerator_management::instanceIdBitMask,
+              5);
+    EXPECT_NE(msg.instance_id & ocp::accelerator_management::requestBitMask, 0);
+    EXPECT_EQ(msg.ocp_version & 0x0F, ocp::accelerator_management::ocpVersion);
+    EXPECT_EQ((msg.ocp_version & 0xF0) >>
+                  ocp::accelerator_management::ocpTypeBitOffset,
+              ocp::accelerator_management::ocpType);
+    EXPECT_EQ(msg.ocp_accelerator_management_msg_type, 0x7E);
+}
+
+// Tests for GpuMctpVdm::encodeQueryDeviceIdentificationRequest function
+TEST_F(GpuMctpVdmTests, EncodeQueryDeviceIdentificationRequestSuccess)
+{
+    const uint8_t instanceId = 3;
+    std::vector<uint8_t> buf(256);
+
+    int result = gpu::encodeQueryDeviceIdentificationRequest(instanceId, buf);
+
+    EXPECT_EQ(result, 0);
+
+    gpu::QueryDeviceIdentificationRequest request{};
+    std::memcpy(&request, buf.data(), sizeof(request));
+
+    EXPECT_EQ(request.hdr.msgHdr.hdr.pci_vendor_id,
+              htobe16(gpu::nvidiaPciVendorId));
+    EXPECT_EQ(request.hdr.msgHdr.hdr.instance_id &
+                  ocp::accelerator_management::instanceIdBitMask,
+              instanceId & ocp::accelerator_management::instanceIdBitMask);
+    EXPECT_NE(request.hdr.msgHdr.hdr.instance_id &
+                  ocp::accelerator_management::requestBitMask,
+              0);
+
+    EXPECT_EQ(request.hdr.command,
+              static_cast<uint8_t>(gpu::DeviceCapabilityDiscoveryCommands::
+                                       QUERY_DEVICE_IDENTIFICATION));
+    EXPECT_EQ(request.hdr.data_size, 0);
+}
+
+// Tests for GpuMctpVdm::decodeQueryDeviceIdentificationResponse function
+TEST_F(GpuMctpVdmTests, DecodeQueryDeviceIdentificationResponseSuccess)
+{
+    // Create a mock successful response
+    std::vector<uint8_t> buf(sizeof(gpu::QueryDeviceIdentificationResponse));
+
+    gpu::QueryDeviceIdentificationResponse response{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 3;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    gpu::packHeader(headerInfo, response.hdr.msgHdr.hdr);
+
+    // Populate response data
+    response.hdr.command = static_cast<uint8_t>(
+        gpu::DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    response.hdr.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    response.hdr.reserved = 0;
+    response.hdr.data_size =
+        htole16(2); // Size of device_identification + instance_id
+    response.device_identification =
+        static_cast<uint8_t>(gpu::DeviceIdentification::DEVICE_GPU);
+    response.instance_id = 7;
+
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    // Test decoding
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    uint8_t deviceIdentification{};
+    uint8_t deviceInstance{};
+
+    int result = gpu::decodeQueryDeviceIdentificationResponse(
+        buf, cc, reasonCode, deviceIdentification, deviceInstance);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(reasonCode, 0);
+    EXPECT_EQ(deviceIdentification,
+              static_cast<uint8_t>(gpu::DeviceIdentification::DEVICE_GPU));
+    EXPECT_EQ(deviceInstance, 7);
+}
+
+TEST_F(GpuMctpVdmTests, DecodeQueryDeviceIdentificationResponseError)
+{
+    // Create a mock successful response
+    std::vector<uint8_t> buf(
+        sizeof(ocp::accelerator_management::CommonNonSuccessResponse));
+
+    ocp::accelerator_management::CommonNonSuccessResponse response{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 3;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    gpu::packHeader(headerInfo, response.msgHdr.hdr);
+
+    // Populate response data
+    response.command = static_cast<uint8_t>(
+        gpu::DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    response.command = static_cast<uint8_t>(
+        gpu::DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    response.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    response.reason_code = htole16(0x1234);
+
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    // Test decoding
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    uint8_t deviceIdentification{};
+    uint8_t deviceInstance{};
+
+    int result = gpu::decodeQueryDeviceIdentificationResponse(
+        buf, cc, reasonCode, deviceIdentification, deviceInstance);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::ERROR);
+    EXPECT_EQ(reasonCode, 0x1234);
+}
+
+TEST_F(GpuMctpVdmTests, DecodeQueryDeviceIdentificationResponseInvalidSize)
+{
+    // Create a too-small buffer
+    std::vector<uint8_t> buf(
+        sizeof(ocp::accelerator_management::Message) + 2); // Too small
+
+    // Populate Message header only
+    ocp::accelerator_management::Message msg{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 3;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    gpu::packHeader(headerInfo, msg.hdr);
+    std::memcpy(buf.data(), &msg, sizeof(msg));
+
+    // Test decoding with insufficient data
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    uint8_t deviceIdentification{};
+    uint8_t deviceInstance{};
+
+    int result = gpu::decodeQueryDeviceIdentificationResponse(
+        buf, cc, reasonCode, deviceIdentification, deviceInstance);
+
+    EXPECT_EQ(result, EINVAL); // Should indicate error for invalid size
+}
+
+// Tests for GpuMctpVdm::encodeGetTemperatureReadingRequest function
+TEST_F(GpuMctpVdmTests, EncodeGetTemperatureReadingRequestSuccess)
+{
+    const uint8_t instanceId = 4;
+    const uint8_t sensorId = 0;
+    std::vector<uint8_t> buf(256);
+
+    int result =
+        gpu::encodeGetTemperatureReadingRequest(instanceId, sensorId, buf);
+
+    EXPECT_EQ(result, 0);
+
+    gpu::GetTemperatureReadingRequest request{};
+    std::memcpy(&request, buf.data(), sizeof(request));
+
+    EXPECT_EQ(request.hdr.msgHdr.hdr.pci_vendor_id,
+              htobe16(gpu::nvidiaPciVendorId));
+    EXPECT_EQ(request.hdr.msgHdr.hdr.instance_id &
+                  ocp::accelerator_management::instanceIdBitMask,
+              instanceId & ocp::accelerator_management::instanceIdBitMask);
+    EXPECT_NE(request.hdr.msgHdr.hdr.instance_id &
+                  ocp::accelerator_management::requestBitMask,
+              0);
+    EXPECT_EQ(request.hdr.msgHdr.hdr.ocp_accelerator_management_msg_type,
+              static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL));
+
+    // Verify request data
+    EXPECT_EQ(request.hdr.command,
+              static_cast<uint8_t>(
+                  gpu::PlatformEnvironmentalCommands::GET_TEMPERATURE_READING));
+    EXPECT_EQ(request.hdr.data_size, sizeof(sensorId));
+    EXPECT_EQ(request.sensor_id, sensorId);
+}
+
+// Tests for GpuMctpVdm::decodeGetTemperatureReadingResponse function
+TEST_F(GpuMctpVdmTests, DecodeGetTemperatureReadingResponseSuccess)
+{
+    // Create a mock successful response
+    std::vector<uint8_t> buf(sizeof(gpu::GetTemperatureReadingResponse));
+
+    gpu::GetTemperatureReadingResponse response{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 4;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL);
+
+    gpu::packHeader(headerInfo, response.hdr.msgHdr.hdr);
+
+    // Populate response data
+    response.hdr.command = static_cast<uint8_t>(
+        gpu::PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    response.hdr.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    response.hdr.reserved = 0;
+    response.hdr.data_size = htole16(sizeof(int32_t));
+
+    // Set a temperature value of 75.5°C (75.5 * 256 = 19328)
+    response.reading = htole32(19328);
+
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    // Test decoding
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    double temperatureReading{};
+
+    int result = gpu::decodeGetTemperatureReadingResponse(
+        buf, cc, reasonCode, temperatureReading);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(reasonCode, 0);
+    EXPECT_NEAR(temperatureReading, 75.5, 0.01);
+}
+
+TEST_F(GpuMctpVdmTests, DecodeGetTemperatureReadingResponseError)
+{
+    std::vector<uint8_t> buf(
+        sizeof(ocp::accelerator_management::CommonNonSuccessResponse));
+
+    // Populate error response data
+    ocp::accelerator_management::CommonNonSuccessResponse errorResponse{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 3;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    gpu::packHeader(headerInfo, errorResponse.msgHdr.hdr);
+
+    errorResponse.command = static_cast<uint8_t>(
+        gpu::PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    errorResponse.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERR_NOT_READY);
+    errorResponse.reason_code = htole16(0x4321);
+
+    std::memcpy(buf.data(), &errorResponse, sizeof(errorResponse));
+
+    // Test decoding
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    double temperatureReading{};
+
+    int result = gpu::decodeGetTemperatureReadingResponse(
+        buf, cc, reasonCode, temperatureReading);
+
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(cc, ocp::accelerator_management::CompletionCode::ERR_NOT_READY);
+    EXPECT_EQ(reasonCode, 0x4321);
+}
+
+TEST_F(GpuMctpVdmTests, DecodeGetTemperatureReadingResponseInvalidSize)
+{
+    // Create a mock response with invalid data_size
+    std::vector<uint8_t> buf(sizeof(gpu::GetTemperatureReadingResponse));
+
+    gpu::GetTemperatureReadingResponse response{};
+    ocp::accelerator_management::BindingPciVidInfo headerInfo{};
+    headerInfo.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    headerInfo.instance_id = 4;
+    headerInfo.msg_type =
+        static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL);
+
+    gpu::packHeader(headerInfo, response.hdr.msgHdr.hdr);
+
+    response.hdr.command = static_cast<uint8_t>(
+        gpu::PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    response.hdr.completion_code = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    response.hdr.reserved = 0;
+    response.hdr.data_size = htole16(1); // Invalid - should be sizeof(int32_t)
+    response.reading = htole32(19328);
+
+    std::memcpy(buf.data(), &response, sizeof(response));
+
+    // Test decoding
+    ocp::accelerator_management::CompletionCode cc{};
+    uint16_t reasonCode{};
+    double temperatureReading{};
+
+    int result = gpu::decodeGetTemperatureReadingResponse(
+        buf, cc, reasonCode, temperatureReading);
+
+    EXPECT_EQ(result, EINVAL); // Should indicate error for invalid data size
+}
+
+} // namespace gpu_mctp_tests
+
+int main(int argc, char** argv)
+{
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/src/nvidia-gpu/tests/meson.build b/src/nvidia-gpu/tests/meson.build
new file mode 100644
index 0000000..4923868
--- /dev/null
+++ b/src/nvidia-gpu/tests/meson.build
@@ -0,0 +1,29 @@
+gtest_dep = dependency('gtest', main: true, disabler: true, required: false)
+gmock_dep = dependency('gmock', disabler: true, required: false)
+if not gtest_dep.found() or not gmock_dep.found()
+    gtest_proj = import('cmake').subproject('googletest', required: true)
+    gtest_dep = declare_dependency(
+        dependencies: [
+            dependency('threads'),
+            gtest_proj.dependency('gtest'),
+            gtest_proj.dependency('gtest_main'),
+        ],
+    )
+    gmock_dep = gtest_proj.dependency('gmock')
+endif
+
+gpusensor_test_include_dirs = [gpusensor_include_dir]
+
+test(
+    'nvidiagpusensor_test',
+    executable(
+        'nvidiagpusensor_test',
+        'NvidiaGpuSensorTest.cpp',
+        '../OcpMctpVdm.cpp',
+        '../NvidiaGpuMctpVdm.cpp',
+        implicit_include_directories: false,
+        include_directories: gpusensor_test_include_dirs,
+        dependencies: [gtest_dep, gmock_dep],
+    ),
+    workdir: meson.current_source_dir(),
+)