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 -
'''
  https://www.opencompute.org/documents/ocp-gpu-accelerator-management-interfaces-v1-pdf
'''

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/+/79312
https://gerrit.openbmc.org/c/openbmc/openbmc/+/79410
https://gerrit.openbmc.org/c/openbmc/openbmc/+/79422

Copy the configuration file on gb200nvl-obmc machine and restart the
entity-manager service.
```
root@gb200nvl-obmc:~# rm -rf /var/configuration/
root@gb200nvl-obmc:~# systemctl restart xyz.openbmc_project.EntityManager.service
```

Copy the gpusensor app and run it.
```
root@gb200nvl-obmc:~# ./gpusensor
```

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
```

Change-Id: Ied938b9e5c19751ee283b4b948e16c905c78fb48
Signed-off-by: Harshit Aghera <haghera@nvidia.com>
diff --git a/.clang-tidy b/.clang-tidy
index ab81074..a5fcf2d 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -376,4 +376,4 @@
   - { key: readability-identifier-naming.NamespaceCase, value: lower_case }
   - { key: readability-identifier-naming.StructCase,    value: CamelCase  }
   - { key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams, value: true }
-  - { key: misc-include-cleaner.IgnoreHeaders, value: ((stdlib.h)|(nlohmann/json_fwd.hpp)|(boost/.*/src.hpp)|(boost/.*/detail/.*)|(nlohmann/detail/.*)|(stdio.h)|(ranges)|(bits/chrono.h)|(boost/system/error_code.hpp)) }
+  - { key: misc-include-cleaner.IgnoreHeaders, value: ((stdlib.h)|(nlohmann/json_fwd.hpp)|(boost/.*/src.hpp)|(boost/.*/detail/.*)|(nlohmann/detail/.*)|(stdio.h)|(ranges)|(bits/chrono.h)|(boost/system/error_code.hpp)|(asm/byteorder.h)) }
diff --git a/src/gpu/GpuMctpVdm.cpp b/src/gpu/GpuMctpVdm.cpp
new file mode 100644
index 0000000..1afc6ca
--- /dev/null
+++ b/src/gpu/GpuMctpVdm.cpp
@@ -0,0 +1,273 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "GpuMctpVdm.hpp"
+
+#include "OcpMctpVdm.hpp"
+
+#include <endian.h>
+
+#include <cstdint>
+#include <cstring>
+
+namespace gpu
+{
+
+ocp::accelerator_management::CompletionCode packHeader(
+    const ocp::accelerator_management::BindingPciVidInfo& hdr,
+    ocp::accelerator_management::BindingPciVid& msg)
+{
+    return ocp::accelerator_management::packHeader(nvidiaPciVendorId, hdr, msg);
+}
+
+ocp::accelerator_management::CompletionCode encodeReasonCode(
+    uint8_t cc, uint16_t reasonCode, uint8_t commandCode,
+    ocp::accelerator_management::Message& msg)
+{
+    return ocp::accelerator_management::encodeReasonCode(cc, reasonCode,
+                                                         commandCode, msg);
+}
+
+ocp::accelerator_management::CompletionCode decodeReasonCodeAndCC(
+    const ocp::accelerator_management::Message& msg, size_t msgLen, uint8_t& cc,
+    uint16_t& reasonCode)
+{
+    if (be16toh(msg.hdr.pci_vendor_id) != nvidiaPciVendorId)
+    {
+        return ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA;
+    }
+
+    return ocp::accelerator_management::decodeReasonCodeAndCC(
+        msg, msgLen, cc, reasonCode);
+}
+
+ocp::accelerator_management::CompletionCode
+    encodeQueryDeviceIdentificationRequest(
+        uint8_t instanceId, ocp::accelerator_management::Message& msg)
+{
+    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::instanceIdMask;
+    header.msg_type =
+        static_cast<uint8_t>(MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    auto rc = packHeader(header, msg.hdr);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    QueryDeviceIdentificationRequest request{};
+    request.hdr.command = static_cast<uint8_t>(
+        DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    request.hdr.data_size = 0;
+
+    std::memcpy(&msg.data, &request, sizeof(request));
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode
+    encodeQueryDeviceIdentificationResponse(
+        uint8_t instanceId, uint8_t cc, uint16_t reasonCode,
+        uint8_t deviceIdentification, uint8_t deviceInstance,
+        ocp::accelerator_management::Message& msg)
+{
+    ocp::accelerator_management::BindingPciVidInfo header{};
+    header.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    header.instance_id = instanceId &
+                         ocp::accelerator_management::instanceIdMask;
+    header.msg_type =
+        static_cast<uint8_t>(MessageType::DEVICE_CAPABILITY_DISCOVERY);
+
+    auto rc = packHeader(header, msg.hdr);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    if (cc != static_cast<uint8_t>(
+                  ocp::accelerator_management::CompletionCode::SUCCESS))
+    {
+        return gpu::encodeReasonCode(
+            cc, reasonCode,
+            static_cast<uint8_t>(
+                DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION),
+            msg);
+    }
+
+    QueryDeviceIdentificationResponse response{};
+    response.hdr.command = static_cast<uint8_t>(
+        DeviceCapabilityDiscoveryCommands::QUERY_DEVICE_IDENTIFICATION);
+    response.hdr.completion_code = cc;
+    response.hdr.data_size = htole16(2);
+    response.device_identification = deviceIdentification;
+    response.instance_id = deviceInstance;
+
+    std::memcpy(&msg.data, &response, sizeof(response));
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode
+    decodeQueryDeviceIdentificationResponse(
+        const ocp::accelerator_management::Message& msg, size_t msgLen,
+        uint8_t& cc, uint16_t& reasonCode, uint8_t& deviceIdentification,
+        uint8_t& deviceInstance)
+{
+    auto rc = gpu::decodeReasonCodeAndCC(msg, msgLen, cc, reasonCode);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS ||
+        cc != static_cast<uint8_t>(
+                  ocp::accelerator_management::CompletionCode::SUCCESS))
+    {
+        return rc;
+    }
+
+    if (msgLen < sizeof(ocp::accelerator_management::BindingPciVid) +
+                     sizeof(QueryDeviceIdentificationResponse))
+    {
+        return ocp::accelerator_management::CompletionCode::
+            ERR_INVALID_DATA_LENGTH;
+    }
+
+    QueryDeviceIdentificationResponse response{};
+    std::memcpy(&response, &msg.data, sizeof(response));
+
+    deviceIdentification = response.device_identification;
+    deviceInstance = response.instance_id;
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode encodeGetTemperatureReadingRequest(
+    uint8_t instanceId, uint8_t sensorId,
+    ocp::accelerator_management::Message& msg)
+{
+    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::instanceIdMask;
+    header.msg_type = static_cast<uint8_t>(MessageType::PLATFORM_ENVIRONMENTAL);
+
+    auto rc = packHeader(header, msg.hdr);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    GetTemperatureReadingRequest request{};
+    request.hdr.command = static_cast<uint8_t>(
+        PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    request.hdr.data_size = sizeof(sensorId);
+    request.sensor_id = sensorId;
+
+    std::memcpy(&msg.data, &request, sizeof(request));
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode decodeGetTemperatureReadingRequest(
+    const ocp::accelerator_management::Message& msg, size_t msgLen,
+    uint8_t& sensorId)
+{
+    if (msgLen < sizeof(ocp::accelerator_management::BindingPciVid) +
+                     sizeof(GetTemperatureReadingRequest))
+    {
+        return ocp::accelerator_management::CompletionCode::
+            ERR_INVALID_DATA_LENGTH;
+    }
+
+    GetTemperatureReadingRequest request{};
+    std::memcpy(&request, &msg.data, sizeof(request));
+
+    if (request.hdr.data_size < sizeof(request.sensor_id))
+    {
+        return ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA;
+    }
+
+    sensorId = request.sensor_id;
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode encodeGetTemperatureReadingResponse(
+    uint8_t instanceId, uint8_t cc, uint16_t reasonCode,
+    double temperatureReading, ocp::accelerator_management::Message& msg)
+{
+    ocp::accelerator_management::BindingPciVidInfo header{};
+    header.ocp_accelerator_management_msg_type = static_cast<uint8_t>(
+        ocp::accelerator_management::MessageType::RESPONSE);
+    header.instance_id = instanceId &
+                         ocp::accelerator_management::instanceIdMask;
+    header.msg_type = static_cast<uint8_t>(MessageType::PLATFORM_ENVIRONMENTAL);
+
+    auto rc = packHeader(header, msg.hdr);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        return rc;
+    }
+
+    if (cc != static_cast<uint8_t>(
+                  ocp::accelerator_management::CompletionCode::SUCCESS))
+    {
+        return gpu::encodeReasonCode(
+            cc, reasonCode,
+            static_cast<uint8_t>(
+                PlatformEnvironmentalCommands::GET_TEMPERATURE_READING),
+            msg);
+    }
+
+    GetTemperatureReadingResponse response{};
+    response.hdr.command = static_cast<uint8_t>(
+        PlatformEnvironmentalCommands::GET_TEMPERATURE_READING);
+    response.hdr.completion_code = cc;
+    response.hdr.data_size = htole16(sizeof(uint32_t));
+
+    int32_t reading = static_cast<int32_t>(temperatureReading * (1 << 8));
+    response.reading = htole32(reading);
+
+    std::memcpy(&msg.data, &response, sizeof(response));
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+
+ocp::accelerator_management::CompletionCode decodeGetTemperatureReadingResponse(
+    const ocp::accelerator_management::Message& msg, size_t msgLen, uint8_t& cc,
+    uint16_t& reasonCode, double& temperatureReading)
+{
+    auto rc = gpu::decodeReasonCodeAndCC(msg, msgLen, cc, reasonCode);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS ||
+        cc != static_cast<uint8_t>(
+                  ocp::accelerator_management::CompletionCode::SUCCESS))
+    {
+        return rc;
+    }
+
+    if (msgLen < sizeof(ocp::accelerator_management::BindingPciVid) +
+                     sizeof(GetTemperatureReadingResponse))
+    {
+        return ocp::accelerator_management::CompletionCode::
+            ERR_INVALID_DATA_LENGTH;
+    }
+
+    GetTemperatureReadingResponse response{};
+    std::memcpy(&response, &msg.data, sizeof(response));
+
+    uint16_t dataSize = le16toh(response.hdr.data_size);
+    if (dataSize != sizeof(int32_t))
+    {
+        return ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA;
+    }
+
+    int32_t reading = le32toh(response.reading);
+    temperatureReading = reading / static_cast<double>(1 << 8);
+
+    return ocp::accelerator_management::CompletionCode::SUCCESS;
+}
+} // namespace gpu
diff --git a/src/gpu/GpuMctpVdm.hpp b/src/gpu/GpuMctpVdm.hpp
new file mode 100644
index 0000000..21c69cd
--- /dev/null
+++ b/src/gpu/GpuMctpVdm.hpp
@@ -0,0 +1,246 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <asm/byteorder.h>
+
+#include <OcpMctpVdm.hpp>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace gpu
+{
+
+/** @brief NVIDIA PCI vendor ID */
+constexpr uint16_t nvidiaPciVendorId = 0x10de;
+
+/** @brief GPU message types
+ *
+ *  Enumeration of different message types used in GPU protocol.
+ *  These types categorize different classes of messages for device management
+ *  and monitoring.
+ */
+enum class MessageType : uint8_t
+{
+    DEVICE_CAPABILITY_DISCOVERY = 0,
+    PLATFORM_ENVIRONMENTAL = 3
+};
+
+/** @brief Type0 Device Capability Discovery Commands
+ */
+enum class DeviceCapabilityDiscoveryCommands : uint8_t
+{
+    QUERY_DEVICE_IDENTIFICATION = 0x09,
+};
+
+/** @brief Type3 platform environmental commands
+ */
+enum class PlatformEnvironmentalCommands : uint8_t
+{
+    GET_TEMPERATURE_READING = 0x00,
+};
+
+/** @brief device identification types
+ *
+ *  Enumeration of different device types that can be identified in the system.
+ *  This is used to distinguish between various components during device
+ * discovery.
+ */
+enum class DeviceIdentification : uint8_t
+{
+    DEVICE_GPU = 0
+};
+
+/** @struct QueryDeviceIdentificationRequest
+ *
+ *  Structure representing query device identification request
+ */
+struct QueryDeviceIdentificationRequest
+{
+    ocp::accelerator_management::CommonRequest hdr;
+} __attribute__((packed));
+
+/** @struct QueryDeviceIdentificationResponse
+ *
+ *  Structure representing query device identification response.
+ */
+struct QueryDeviceIdentificationResponse
+{
+    ocp::accelerator_management::CommonResponse hdr;
+    uint8_t device_identification;
+    uint8_t instance_id;
+} __attribute__((packed));
+
+/** @struct GetNumericSensorReadingRequest
+ *
+ *  Structure representing request to get reading of certain numeric
+ * sensors.
+ */
+struct GetNumericSensorReadingRequest
+{
+    ocp::accelerator_management::CommonRequest hdr;
+    uint8_t sensor_id;
+} __attribute__((packed));
+
+/** @struct GetTemperatureReadingRequest
+ *
+ *  Structure representing get temperature reading request.
+ */
+using GetTemperatureReadingRequest = GetNumericSensorReadingRequest;
+
+/** @struct GetTemperatureReadingResponse
+ *
+ *  Structure representing get temperature reading response.
+ */
+struct GetTemperatureReadingResponse
+{
+    ocp::accelerator_management::CommonResponse hdr;
+    int32_t reading;
+} __attribute__((packed));
+
+/**
+ * @brief Populate the GPU message with the GPU header.
+ *        The caller of this API allocates buffer for the GPU header
+ *        when forming the GPU message.
+ *        The buffer is passed to this API to pack the GPU header.
+ *
+ * @param[in] hdr - Reference to the OCP MCTP VDM header information
+ * @param[out] msg - Reference to GPU message header
+ *
+ * @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ * otherwise appropriate error code.
+ * @note   Caller is responsible for alloc and dealloc of msg
+ *         and hdr params
+ */
+ocp::accelerator_management::CompletionCode packHeader(
+    const ocp::accelerator_management::BindingPciVidInfo& hdr,
+    ocp::accelerator_management::BindingPciVid& msg);
+
+/** @brief Encode reason code
+ *
+ *  @param[in] cc - Completion Code
+ *  @param[in] reason_code - reason code
+ *  @param[in] command_code - command code
+ *  @param[out] msg - Reference to message
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode encodeReasonCode(
+    uint8_t cc, uint16_t reasonCode, uint8_t commandCode,
+    ocp::accelerator_management::Message& msg);
+
+/** @brief Decode to get reason code
+ *
+ *  @param[in] msg - response message
+ *  @param[in] msg_len - Length of response message
+ *  @param[out] cc - reference to completion code
+ *  @param[out] reason_code - reference to reason_code
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode decodeReasonCodeAndCC(
+    const ocp::accelerator_management::Message& msg, size_t msgLen, uint8_t& cc,
+    uint16_t& reasonCode);
+
+/** @brief Create a Query device identification request message
+ *
+ *  @param[in] instance_id - instance ID
+ *  @param[out] msg - Reference to message that will be written to
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode
+    encodeQueryDeviceIdentificationRequest(
+        uint8_t instanceId, ocp::accelerator_management::Message& msg);
+
+/** @brief Encode a Query device identification response message
+ *
+ *  @param[in] instance_id - instance ID
+ *  @param[in] cc - completion code
+ *  @param[in] reason_code - reason code
+ *  @param[in] device_identification - device identification
+ *  @param[in] device_instance - device instance id
+ *  @param[out] msg - Reference to message that will be written to
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode
+    encodeQueryDeviceIdentificationResponse(
+        uint8_t instanceId, uint8_t cc, uint16_t reasonCode,
+        uint8_t deviceIdentification, uint8_t deviceInstance,
+        ocp::accelerator_management::Message& msg);
+
+/** @brief Decode a Query device identification response message
+ *
+ *  @param[in] msg - response message
+ *  @param[in] msg_len - Length of response message
+ *  @param[out] cc - reference to completion code
+ *  @param[out] reason_code - reference to reason code
+ *  @param[out] device_identification - reference to device_identification
+ *  @param[out] device_instance - reference to instance id
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode
+    decodeQueryDeviceIdentificationResponse(
+        const ocp::accelerator_management::Message& msg, size_t msgLen,
+        uint8_t& cc, uint16_t& reasonCode, uint8_t& deviceIdentification,
+        uint8_t& deviceInstance);
+
+/** @brief Encode a Get temperature readings request message
+ *
+ *  @param[in] instance_id - instance ID
+ *  @param[in] sensor_id - sensor id
+ *  @param[out] msg - Reference to message that will be written to
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode encodeGetTemperatureReadingRequest(
+    uint8_t instanceId, uint8_t sensorId,
+    ocp::accelerator_management::Message& msg);
+
+/** @brief Decode a Get temperature readings request message
+ *
+ *  @param[in] msg - request message
+ *  @param[in] msg_len - Length of request message
+ *  @param[out] sensor_id - reference to sensor id
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode decodeGetTemperatureReadingRequest(
+    const ocp::accelerator_management::Message& msg, size_t msgLen,
+    uint8_t& sensorId);
+
+/** @brief Encode a Get temperature readings response message
+ *
+ *  @param[in] instance_id - instance ID
+ *  @param[in] cc - pointer to response message completion code
+ *  @param[in] reason_code - reason code
+ *  @param[in] temperature_reading - temperature reading
+ *  @param[out] msg - Reference to message that will be written to
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode encodeGetTemperatureReadingResponse(
+    uint8_t instanceId, uint8_t cc, uint16_t reasonCode,
+    double temperatureReading, ocp::accelerator_management::Message& msg);
+
+/** @brief Decode a Get temperature readings response message
+ *
+ *  @param[in] msg - response message
+ *  @param[in] msg_len - Length of response message
+ *  @param[out] cc - reference to response message completion code
+ *  @param[out] reason_code - reference to reason code
+ *  @param[out] temperature_reading - reference to temperature_reading
+ *  @return ocp::accelerator_management::CompletionCode::SUCCESS on success,
+ *  otherwise appropriate error code.
+ */
+ocp::accelerator_management::CompletionCode decodeGetTemperatureReadingResponse(
+    const ocp::accelerator_management::Message& msg, size_t msgLen, uint8_t& cc,
+    uint16_t& reasonCode, double& temperatureReading);
+
+} // namespace gpu
diff --git a/src/gpu/GpuSensor.cpp b/src/gpu/GpuSensor.cpp
index ed81339..119554d 100644
--- a/src/gpu/GpuSensor.cpp
+++ b/src/gpu/GpuSensor.cpp
@@ -5,12 +5,16 @@
 
 #include "GpuSensor.hpp"
 
+#include "SensorPaths.hpp"
 #include "Thresholds.hpp"
 #include "Utils.hpp"
 #include "sensor.hpp"
 
 #include <bits/basic_string.h>
 
+#include <GpuMctpVdm.hpp>
+#include <MctpRequester.hpp>
+#include <OcpMctpVdm.hpp>
 #include <boost/asio/io_context.hpp>
 #include <boost/container/flat_map.hpp>
 #include <phosphor-logging/lg2.hpp>
@@ -23,6 +27,7 @@
 #include <chrono>
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <map>
 #include <memory>
 #include <string>
@@ -32,20 +37,24 @@
 
 using namespace std::literals;
 
+constexpr uint8_t gpuTempSensorId{0};
+constexpr std::chrono::milliseconds samplingInterval{1000ms};
 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);
@@ -86,6 +95,147 @@
     discoverGpus();
 }
 
+void GpuTempSensor::read()
+{
+    update();
+
+    waitTimer.expires_after(std::chrono::milliseconds(sensorPollMs));
+    waitTimer.async_wait([this](const boost::system::error_code& ec) {
+        if (ec)
+        {
+            return;
+        }
+        read();
+    });
+}
+
+void GpuTempSensor::update()
+{
+    std::vector<uint8_t> reqMsg(
+        sizeof(ocp::accelerator_management::BindingPciVid) +
+        sizeof(gpu::GetTemperatureReadingRequest));
+
+    auto* msg = new (reqMsg.data()) ocp::accelerator_management::Message;
+
+    auto rc = gpu::encodeGetTemperatureReadingRequest(0, sensorId, *msg);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        lg2::error(
+            "GpuTempSensor::update(): gpuEncodeGetTemperatureReadingRequest failed, rc={RC}",
+            "RC", static_cast<int>(rc));
+        return;
+    }
+
+    mctpRequester.sendRecvMsg(
+        eid, reqMsg,
+        [this](int sendRecvMsgResult, std::vector<uint8_t> respMsg) {
+            if (sendRecvMsgResult != 0)
+            {
+                lg2::error(
+                    "GpuTempSensor::update(): MctpRequester::sendRecvMsg() failed, rc={RC}",
+                    "RC", sendRecvMsgResult);
+                return;
+            }
+
+            if (respMsg.empty())
+            {
+                lg2::error(
+                    "GpuTempSensor::update(): MctpRequester::sendRecvMsg() failed, respMsgLen=0");
+                return;
+            }
+
+            uint8_t cc = 0;
+            uint16_t reasonCode = 0;
+            double tempValue = 0;
+
+            auto rc = gpu::decodeGetTemperatureReadingResponse(
+                *new (respMsg.data()) ocp::accelerator_management::Message,
+                respMsg.size(), cc, reasonCode, tempValue);
+
+            if (rc != ocp::accelerator_management::CompletionCode::SUCCESS ||
+                cc != static_cast<uint8_t>(
+                          ocp::accelerator_management::CompletionCode::SUCCESS))
+            {
+                lg2::error(
+                    "GpuTempSensor::update(): gpuDecodeGetTemperatureReadingResponse() failed, rc={RC} cc={CC} reasonCode={RESC}",
+                    "RC", static_cast<int>(rc), "CC", cc, "RESC", reasonCode);
+                return;
+            }
+
+            updateValue(tempValue);
+        });
+}
+
+void GpuTempSensor::processGpuEndpoint(uint8_t eid)
+{
+    std::vector<uint8_t> reqMsg(
+        sizeof(ocp::accelerator_management::BindingPciVid) +
+        sizeof(gpu::QueryDeviceIdentificationRequest));
+
+    auto* msg = new (reqMsg.data()) ocp::accelerator_management::Message;
+
+    auto rc = gpu::encodeQueryDeviceIdentificationRequest(0, *msg);
+    if (rc != ocp::accelerator_management::CompletionCode::SUCCESS)
+    {
+        lg2::error(
+            "GpuTempSensor::processGpuEndPoint(): gpuEncodeQueryDeviceIdentificationRequest failed, rc={RC}",
+            "RC", static_cast<int>(rc));
+        return;
+    }
+
+    mctpRequester.sendRecvMsg(
+        eid, reqMsg,
+        [this, eid](int sendRecvMsgResult, std::vector<uint8_t> respMsg) {
+            if (sendRecvMsgResult != 0)
+            {
+                lg2::error(
+                    "GpuTempSensor::processGpuEndPoint(): MctpRequester::sendRecvMsg() failed, rc={RC}",
+                    "RC", sendRecvMsgResult);
+                return;
+            }
+
+            if (respMsg.empty())
+            {
+                lg2::error(
+                    "GpuTempSensor::processGpuEndPoint(): MctpRequester::sendRecvMsg() failed, respMsgLen=0");
+                return;
+            }
+
+            uint8_t cc = 0;
+            uint16_t reasonCode = 0;
+            uint8_t responseDeviceType = 0;
+            uint8_t responseInstanceId = 0;
+
+            auto rc = gpu::decodeQueryDeviceIdentificationResponse(
+                *new (respMsg.data()) ocp::accelerator_management::Message,
+                respMsg.size(), cc, reasonCode, responseDeviceType,
+                responseInstanceId);
+
+            if (rc != ocp::accelerator_management::CompletionCode::SUCCESS ||
+                cc != static_cast<uint8_t>(
+                          ocp::accelerator_management::CompletionCode::SUCCESS))
+            {
+                lg2::error(
+                    "GpuTempSensor::processGpuEndPoint(): gpuDecodeQueryDeviceIdentificationResponse() failed, rc={RC} cc={CC} reasonCode={RESC}",
+                    "RC", static_cast<int>(rc), "CC", cc, "RESC", reasonCode);
+                return;
+            }
+
+            if (responseDeviceType ==
+                static_cast<uint8_t>(gpu::DeviceIdentification::DEVICE_GPU))
+            {
+                lg2::info(
+                    "GpuTempSensor::processGpuEndPoint(): found the GPU with EID {EID}, DeviceType {DEVTYPE}, InstanceId {IID}.",
+                    "EID", eid, "DEVTYPE", responseDeviceType, "IID",
+                    responseInstanceId);
+
+                this->eid = eid;
+                setInitialProperties(sensor_paths::unitDegreesC);
+                this->read();
+            }
+        });
+}
+
 void GpuTempSensor::processMctpEndpoints(const boost::system::error_code& ec,
                                          const getSubTreeRet& ret)
 {
@@ -132,7 +282,7 @@
         return;
     }
 
-    [[maybe_unused]] uint8_t eid{};
+    uint8_t eid{};
     std::vector<uint8_t> mctpTypes{};
 
     auto hasEid = configs.find("EID");
@@ -180,9 +330,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()
@@ -205,7 +360,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)
     {
@@ -219,8 +374,8 @@
             std::string name = loadVariant<std::string>(cfg, "Name");
 
             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>{}, samplingInterval);
 
             lg2::info(
                 "Added GPU Temperature Sensor {NAME} with chassis path: {PATH}.",
@@ -233,7 +388,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)
     {
@@ -241,9 +397,8 @@
         return;
     }
     dbusConnection->async_method_call(
-        [&sensors, &dbusConnection, &io,
-         &objectServer](const boost::system::error_code& ec,
-                        const ManagedObjectType& resp) {
+        [&sensors, &mctpRequester, &dbusConnection, &io, &objectServer](
+            boost::system::error_code ec, const ManagedObjectType& resp) {
             if (ec)
             {
                 lg2::error("Error contacting entity manager");
@@ -251,7 +406,7 @@
             }
 
             processSensorConfigs(io, objectServer, sensors, dbusConnection,
-                                 resp);
+                                 mctpRequester, resp);
         },
         entityManagerName, "/xyz/openbmc_project/inventory",
         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
diff --git a/src/gpu/GpuSensor.hpp b/src/gpu/GpuSensor.hpp
index 01eb23f..7c70d55 100644
--- a/src/gpu/GpuSensor.hpp
+++ b/src/gpu/GpuSensor.hpp
@@ -5,6 +5,7 @@
 
 #pragma once
 
+#include "MctpRequester.hpp"
 #include "Thresholds.hpp"
 #include "sensor.hpp"
 
@@ -15,6 +16,7 @@
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/message.hpp>
 
+#include <chrono>
 #include <cstdint>
 #include <map>
 #include <memory>
@@ -68,10 +70,12 @@
      * @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
@@ -87,11 +91,21 @@
 
   private:
     /**
+     * @brief Read the current temperature value from the GPU
+     */
+    void read();
+
+    /**
      * @brief Initialize the sensor
      */
     void init();
 
     /**
+     * @brief Update the sensor reading
+     */
+    void update();
+
+    /**
      * @brief Discover available GPUs on the system
      */
     void discoverGpus();
@@ -113,6 +127,26 @@
      */
     void processEndpointConfigs(const boost::system::error_code& ec,
                                 const GpuSensorConfigMap& configs);
+    /**
+     * @brief Process a discovered GPU endpoint
+     * @param eid The endpoint ID of the discovered GPU
+     */
+    void processGpuEndpoint(uint8_t eid);
+
+    /**
+     * @brief MCTP endpoint ID
+     */
+    uint8_t eid{};
+
+    /**
+     * @brief The sensor ID
+     */
+    uint8_t sensorId;
+
+    /**
+     * @brief How often to poll the sensor in milliseconds
+     */
+    std::chrono::milliseconds sensorPollMs;
 
     /**
      * @brief Timer for scheduling sensor reads
@@ -120,6 +154,11 @@
     boost::asio::steady_timer waitTimer;
 
     /**
+     * @brief Reference to the MCTP requester for communication
+     */
+    mctp::MctpRequester& mctpRequester;
+
+    /**
      * @brief D-Bus connection
      */
     std::shared_ptr<sdbusplus::asio::connection> conn;
@@ -136,12 +175,14 @@
  * @param objectServer D-Bus object server
  * @param sensors Map to store created sensors
  * @param dbusConnection D-Bus connection
+ * @param mctpRequester MCTP requester for GPU communication
  */
 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
diff --git a/src/gpu/GpuSensorMain.cpp b/src/gpu/GpuSensorMain.cpp
index a7b1d7f..254a11a 100644
--- a/src/gpu/GpuSensorMain.cpp
+++ b/src/gpu/GpuSensorMain.cpp
@@ -4,6 +4,8 @@
  */
 
 #include "GpuSensor.hpp"
+#include "MctpRequester.hpp"
+#include "OcpMctpVdm.hpp"
 #include "Utils.hpp"
 
 #include <boost/asio/error.hpp>
@@ -32,18 +34,19 @@
  * @param io Boost ASIO I/O context
  * @param objectServer D-Bus object server
  * @param dbusConnection D-Bus connection
+ * @param mctpRequester MCTP requester for GPU communication
  * @param ec Boost ASIO error code
  */
 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");
@@ -58,19 +61,23 @@
     objectServer.add_manager("/xyz/openbmc_project/sensors");
     systemBus->request_name("xyz.openbmc_project.GpuSensor");
 
+    mctp::MctpRequester mctpRequester(io,
+                                      ocp::accelerator_management::messageType);
+
     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/gpu/MctpRequester.cpp b/src/gpu/MctpRequester.cpp
new file mode 100644
index 0000000..912e14d
--- /dev/null
+++ b/src/gpu/MctpRequester.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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 <unistd.h>
+
+#include <OcpMctpVdm.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/local/datagram_protocol.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <memory>
+#include <utility>
+#include <vector>
+
+using namespace std::literals;
+
+namespace mctp
+{
+
+MctpRequester::MctpRequester(boost::asio::io_context& ctx, uint8_t msgType) :
+    ctx(ctx), sockfd(socket(AF_MCTP, SOCK_DGRAM, 0)), mctpSocket(ctx),
+    msgType(msgType)
+{
+    if (sockfd < 0)
+    {
+        lg2::error("Failed to create MCTP socket");
+        return;
+    }
+
+    boost::system::error_code ec;
+    mctpSocket.assign(boost::asio::local::datagram_protocol{}, sockfd, ec);
+
+    if (ec)
+    {
+        lg2::error(
+            "MctpRequester failed to connect to the MCTP socket - ErrorCode={EC}, Error={ER}.",
+            "EC", ec.value(), "ER", ec.message());
+        close(sockfd);
+        return;
+    }
+
+    mctpSocket.non_blocking(true);
+}
+
+void MctpRequester::processRecvMsg(
+    mctp_eid_t eid, const std::vector<uint8_t>& reqMsg,
+    const std::function<void(int, std::vector<uint8_t>)>& callback,
+    size_t peekedLength) const
+{
+    // Receive message
+    struct sockaddr sockAddr{};
+    struct sockaddr_mctp respAddr{};
+    socklen_t addrlen = sizeof(respAddr);
+    size_t receivedLength = 0;
+
+    std::vector<uint8_t> fullRespMsg(peekedLength);
+
+    receivedLength = recvfrom(sockfd, fullRespMsg.data(), peekedLength,
+                              MSG_TRUNC, &sockAddr, &addrlen);
+
+    std::memcpy(&respAddr, &sockAddr, sizeof(respAddr));
+
+    if (receivedLength <= 0)
+    {
+        lg2::error("MctpRequester: Failed to receive message");
+        callback(-2, std::vector<uint8_t>{});
+        return;
+    }
+
+    if (respAddr.smctp_type != msgType)
+    {
+        lg2::error("MctpRequester: Message type mismatch");
+        callback(-3, std::move(fullRespMsg));
+        return;
+    }
+
+    mctp_eid_t respEid = respAddr.smctp_addr.s_addr;
+
+    if (respEid != eid)
+    {
+        lg2::error(
+            "MctpRequester: EID mismatch - expected={EID}, received={REID}",
+            "EID", eid, "REID", respEid);
+        callback(-4, std::move(fullRespMsg));
+        return;
+    }
+
+    if (receivedLength > sizeof(ocp::accelerator_management::BindingPciVid))
+    {
+        ocp::accelerator_management::BindingPciVid reqHdr{};
+        std::memcpy(&reqHdr, reqMsg.data(),
+                    sizeof(ocp::accelerator_management::BindingPciVid));
+
+        ocp::accelerator_management::BindingPciVid respHdr{};
+        std::memcpy(&respHdr, fullRespMsg.data(),
+                    sizeof(ocp::accelerator_management::BindingPciVid));
+
+        if (reqHdr.instance_id != respHdr.instance_id)
+        {
+            lg2::error(
+                "MctpRequester: Instance ID mismatch - request={REQ}, response={RESP}",
+                "REQ", static_cast<int>(reqHdr.instance_id), "RESP",
+                static_cast<int>(respHdr.instance_id));
+            callback(-5, std::move(fullRespMsg));
+            return;
+        }
+    }
+
+    callback(0, std::move(fullRespMsg));
+}
+
+void MctpRequester::sendRecvMsg(
+    mctp_eid_t eid, const std::vector<uint8_t>& reqMsg,
+    const std::function<void(int, std::vector<uint8_t>)>& callback)
+{
+    std::vector<uint8_t> respMsg{};
+
+    if (reqMsg.size() < sizeof(ocp::accelerator_management::BindingPciVid))
+    {
+        lg2::error("MctpRequester: Message too small");
+        callback(-2, respMsg);
+        return;
+    }
+
+    // Create address structure
+    struct sockaddr sockAddr{};
+    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;
+
+    std::memcpy(&sockAddr, &addr, sizeof(addr));
+
+    // Send message
+    ssize_t rc = sendto(sockfd, reqMsg.data(), reqMsg.size(), 0, &sockAddr,
+                        sizeof(addr));
+    if (rc < 0)
+    {
+        lg2::error(
+            "MctpRequester failed send data to the MCTP Socket - Error={EC}.",
+            "EC", rc);
+        callback(rc, respMsg);
+        return;
+    }
+
+    // Set up async receive with timeout
+    auto timer = std::make_shared<boost::asio::steady_timer>(ctx);
+    timer->expires_after(2s);
+
+    // Set up handler for when the timer expires
+    timer->async_wait([callback, timer](const boost::system::error_code& ec) {
+        if (ec != boost::asio::error::operation_aborted)
+        {
+            callback(-1, std::vector<uint8_t>{});
+        }
+    });
+
+    // Set up asynchronous receive
+    mctpSocket.async_receive(
+        boost::asio::buffer(respMsg), MSG_PEEK | MSG_TRUNC,
+        [this, eid, reqMsg, callback,
+         timer](const boost::system::error_code& ec, size_t peekedLength) {
+            // Cancel the timer since we got a response
+            timer->cancel();
+
+            if (ec)
+            {
+                lg2::error(
+                    "MctpRequester failed to receive data from the MCTP socket - ErrorCode={EC}, Error={ER}.",
+                    "EC", ec.value(), "ER", ec.message());
+                callback(-1, std::vector<uint8_t>{});
+                return;
+            }
+
+            this->processRecvMsg(eid, reqMsg, callback, peekedLength);
+        });
+}
+
+} // namespace mctp
diff --git a/src/gpu/MctpRequester.hpp b/src/gpu/MctpRequester.hpp
new file mode 100644
index 0000000..63a5ba6
--- /dev/null
+++ b/src/gpu/MctpRequester.hpp
@@ -0,0 +1,94 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/local/datagram_protocol.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <vector>
+
+// Define MCTP EID type
+using mctp_eid_t = uint8_t;
+
+namespace mctp
+{
+/**
+ * @brief MCTP requester class
+ *
+ * This class provides a simple interface for sending and receiving MCTP
+ * messages.
+ */
+class MctpRequester
+{
+  public:
+    MctpRequester() = delete;
+
+    MctpRequester(const MctpRequester&) = delete;
+
+    MctpRequester(MctpRequester&&) = delete;
+
+    MctpRequester& operator=(const MctpRequester&) = delete;
+
+    MctpRequester& operator=(MctpRequester&&) = delete;
+
+    /**
+     * @brief Constructor
+     * @param ctx - The IO context to use
+     * @param msgType - The message type to use
+     */
+    MctpRequester(boost::asio::io_context& ctx, uint8_t msgType);
+
+    /**
+     * @brief Send an MCTP request message and receive the response
+     *
+     * This function sends a request message to the specified endpoint ID and
+     * asynchronously waits for a response. It uses the MCTP socket to handle
+     * the communication. Results are provided via the callback.
+     *
+     * @param[in] eid - The endpoint ID to send the message to
+     * @param[in] reqMsg - The request message to send
+     * @param[in] callback - Callback function to be invoked when response is
+     * received The callback takes two parameters:
+     *            - An integer status code (0 for success, negative for error)
+     *            - A vector containing the response message bytes
+     */
+    void sendRecvMsg(
+        mctp_eid_t eid, const std::vector<uint8_t>& reqMsg,
+        const std::function<void(int, std::vector<uint8_t>)>& callback);
+
+  private:
+    /**
+     * @brief Process received message
+     *
+     * This function processes a received message and invokes the callback with
+     * the appropriate status code and response message bytes.
+     *
+     * @param[in] eid - The endpoint ID from which the message was received
+     * @param[in] reqMsg - The received request message bytes
+     * @param[in] callback - The callback function to invoke with the result
+     * @param[in] peekedLength - The length of the peeked data
+     */
+    void processRecvMsg(
+        mctp_eid_t eid, const std::vector<uint8_t>& reqMsg,
+        const std::function<void(int, std::vector<uint8_t>)>& callback,
+        size_t peekedLength) const;
+
+    /** @brief IO context to use */
+    boost::asio::io_context& ctx;
+
+    /** @brief Socket file descriptor */
+    int sockfd = -1;
+
+    /** @brief Local socket */
+    boost::asio::local::datagram_protocol::socket mctpSocket;
+
+    /** @brief MCTP message type */
+    uint8_t msgType;
+};
+} // namespace mctp
diff --git a/src/gpu/OcpMctpVdm.cpp b/src/gpu/OcpMctpVdm.cpp
new file mode 100644
index 0000000..c6ede8f
--- /dev/null
+++ b/src/gpu/OcpMctpVdm.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 <cstdint>
+#include <cstring>
+
+namespace ocp
+{
+namespace accelerator_management
+{
+
+CompletionCode 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 CompletionCode::ERR_INVALID_DATA;
+    }
+
+    if (hdr.instance_id > instanceMax)
+    {
+        return CompletionCode::ERR_INVALID_DATA;
+    }
+
+    msg.datagram = 0;
+
+    msg.request = 0;
+    if (hdr.ocp_accelerator_management_msg_type ==
+        static_cast<uint8_t>(MessageType::REQUEST))
+    {
+        msg.request = 1;
+    }
+
+    msg.pci_vendor_id = htobe16(pciVendorId);
+    msg.reserved = 0;
+    msg.instance_id = hdr.instance_id;
+    msg.ocp_type = type;
+    msg.ocp_version = version;
+    msg.ocp_accelerator_management_msg_type = hdr.msg_type;
+
+    return CompletionCode::SUCCESS;
+}
+
+CompletionCode encodeReasonCode(uint8_t cc, uint16_t reasonCode,
+                                uint8_t commandCode, Message& msg)
+{
+    CommonNonSuccessResponse response{};
+    response.command = commandCode;
+    response.completion_code = cc;
+    reasonCode = htole16(reasonCode);
+    response.reason_code = reasonCode;
+
+    std::memcpy(&msg.data, &response, sizeof(response));
+
+    return CompletionCode::SUCCESS;
+}
+
+CompletionCode decodeReasonCodeAndCC(const Message& msg, size_t msgLen,
+                                     uint8_t& cc, uint16_t& reasonCode)
+{
+    CommonNonSuccessResponse response{};
+    std::memcpy(&response, &msg.data, sizeof(response));
+
+    cc = response.completion_code;
+    if (cc == static_cast<uint8_t>(CompletionCode::SUCCESS))
+    {
+        return CompletionCode::SUCCESS;
+    }
+
+    if (msgLen != (sizeof(BindingPciVid) + sizeof(CommonNonSuccessResponse)))
+    {
+        return CompletionCode::ERR_INVALID_DATA_LENGTH;
+    }
+
+    // reason code is expected to be present if CC != SUCCESS
+    reasonCode = le16toh(response.reason_code);
+
+    return CompletionCode::SUCCESS;
+}
+} // namespace accelerator_management
+} // namespace ocp
diff --git a/src/gpu/OcpMctpVdm.hpp b/src/gpu/OcpMctpVdm.hpp
new file mode 100644
index 0000000..8c6a0b4
--- /dev/null
+++ b/src/gpu/OcpMctpVdm.hpp
@@ -0,0 +1,215 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#pragma once
+
+#include <asm/byteorder.h>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace ocp
+{
+namespace accelerator_management
+{
+
+/** @brief OCP MCTP VDM Message Type
+ *
+ *  v1 spec section 3.6.1.2.1
+ */
+constexpr uint8_t messageType = 0x7E;
+
+/**
+ * @defgroup OCP Version
+ *
+ * v1 spec section 3.6.1.2.1
+ * @{
+ */
+constexpr uint8_t type = 8;
+constexpr uint8_t version = 9;
+/** @} */
+
+/**
+ * @defgroup OCP MCTP VDM Instance Id
+ *
+ * v1 spec section 3.6.1.2.1
+ * @{
+ */
+constexpr uint8_t instanceMin = 0;
+constexpr uint8_t instanceIdMask = 0x1F;
+constexpr uint8_t instanceMax = 31;
+/** @} */
+
+/** @brief OCP MCTP VDM completion codes
+ *
+ *  v1 spec section 3.6.2
+ */
+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,
+};
+
+/** @brief OCP MCTP VDM reason codes
+ *
+ *  v1 spec section 3.6.3
+ */
+enum class ReasonCode : uint16_t
+{
+    REASON_NONE = 0x00,
+};
+
+/** @brief OCP MCTP VDM MessageType
+ *
+ *  v1 spec section 3.6.1.2.1
+ */
+enum class MessageType : uint8_t
+{
+    RESPONSE = 0, //!< OCP MCTP VDM response message
+    REQUEST = 2,  //!< OCP MCTP VDM request message
+};
+
+/** @struct BindingPciVid
+ *
+ * Structure representing OCP MCTP VDM VDM binding using PCI vendor ID
+ * v1 spec section 3.6.1.2
+ */
+struct BindingPciVid
+{
+    uint16_t pci_vendor_id; //!< PCI defined vendor ID
+
+#if defined(__LITTLE_ENDIAN_BITFIELD)
+    uint8_t instance_id:5; //!< Instance ID
+    uint8_t reserved:1;    //!< Reserved
+    uint8_t datagram:1;    //!< Datagram bit
+    uint8_t request:1;     //!< Request bit
+#elif defined(__BIG_ENDIAN_BITFIELD)
+    uint8_t request:1;     //!< Request bit
+    uint8_t datagram:1;    //!< Datagram bit
+    uint8_t reserved:1;    //!< Reserved
+    uint8_t instance_id:5; //!< Instance ID
+#endif
+
+#if defined(__LITTLE_ENDIAN_BITFIELD)
+    uint8_t ocp_version:4; //!< OCP version
+    uint8_t ocp_type:4;    //!< OCP type
+#elif defined(__BIG_ENDIAN_BITFIELD)
+    uint8_t ocp_type:4;    //!< OCP type
+    uint8_t ocp_version:4; //!< OCP version
+#endif
+
+    uint8_t ocp_accelerator_management_msg_type; //!< Message Type
+} __attribute__((packed));
+
+/** @struct Message
+ *
+ * Structure representing OCP MCTP VDM message
+ * v1 spec section 3.6.1.2
+ */
+struct Message
+{
+    BindingPciVid hdr; //!< OCP MCTP VDM message header
+    char data;         //!< beginning of the payload
+} __attribute__((packed));
+
+/** @struct BindingPciVidInfo
+ *
+ * The information needed to prepare OCP MCTP VDM header and this is passed to
+ * the PackHeader API. v1 spec section 3.6.1.2
+ */
+struct BindingPciVidInfo
+{
+    uint8_t ocp_accelerator_management_msg_type;
+    uint8_t instance_id;
+    uint8_t msg_type;
+};
+
+/** @struct CommonRequest
+ *
+ * Structure representing OCP MCTP VDM request without data (OCP version 1).
+ * v1 spec section 3.6.1.4.1
+ */
+struct CommonRequest
+{
+    uint8_t command;
+    uint8_t data_size;
+} __attribute__((packed));
+
+/** @struct CommonResponse
+ *
+ * Structure representing OCP MCTP VDM response with data
+ * v1 spec section 3.6.1.4.4
+ */
+struct CommonResponse
+{
+    uint8_t command;
+    uint8_t completion_code;
+    uint16_t reserved;
+    uint16_t data_size;
+} __attribute__((packed));
+
+/** @struct CommonNonSuccessResponse
+ *
+ * Structure representing OCP MCTP VDM response with reason code when CC !=
+ * Success v1 spec section 3.6.1.4.5
+ */
+struct CommonNonSuccessResponse
+{
+    uint8_t command;
+    uint8_t completion_code;
+    uint16_t reason_code;
+} __attribute__((packed));
+
+/**
+ * @brief Populate the OCP MCTP VDM message with the OCP MCTP VDM header. OCP
+ * MCTP VDM header OCP Version will be populated with value 1. The caller of
+ * this API allocates buffer for the OCP MCTP VDM header when forming the OCP
+ * MCTP VDM message. The buffer is passed to this API to pack the OCP MCTP VDM
+ * header.
+ *
+ * @param[in] pci_vendor_id - PCI Vendor ID
+ * @param[in] hdr - Pointer to the OCP MCTP VDM header information
+ * @param[out] msg - Reference to OCP MCTP VDM message header
+ *
+ * @return CompletionCode::SUCCESS on success, otherwise appropriate error
+ * code.
+ * @note   Caller is responsible for alloc and dealloc of msg
+ *         and hdr params
+ */
+CompletionCode packHeader(uint16_t pciVendorId, const BindingPciVidInfo& hdr,
+                          BindingPciVid& msg);
+
+/** @brief Encode reason code into an OCP MCTP VDM response message.
+ *         This function does not populate or modifies the message header.
+ *
+ *  @param[in] cc - Completion Code
+ *  @param[in] reason_code - reason code
+ *  @param[in] command_code - command code
+ *  @param[out] msg - Reference to message
+ *  @return CompletionCode
+ */
+CompletionCode encodeReasonCode(uint8_t cc, uint16_t reasonCode,
+                                uint8_t commandCode, Message& msg);
+
+/** @brief Decode the reason code
+ *
+ *  @param[in] msg - response message
+ *  @param[in] msg_len - Length of response message
+ *  @param[out] cc - reference to completion code
+ *  @param[out] reason_code - reference to reason_code
+ *  @return CompletionCode
+ */
+CompletionCode decodeReasonCodeAndCC(const Message& msg, size_t msgLen,
+                                     uint8_t& cc, uint16_t& reasonCode);
+
+} // namespace accelerator_management
+} // namespace ocp
diff --git a/src/gpu/meson.build b/src/gpu/meson.build
index df682e5..1ec72c4 100644
--- a/src/gpu/meson.build
+++ b/src/gpu/meson.build
@@ -1,4 +1,10 @@
-gpusensor_sources = files('GpuSensor.cpp', 'GpuSensorMain.cpp')
+gpusensor_sources = files(
+    'GpuMctpVdm.cpp',
+    'GpuSensor.cpp',
+    'GpuSensorMain.cpp',
+    'MctpRequester.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/gpu/tests/GpuSensorTest.cpp b/src/gpu/tests/GpuSensorTest.cpp
new file mode 100644
index 0000000..e4d6d34
--- /dev/null
+++ b/src/gpu/tests/GpuSensorTest.cpp
@@ -0,0 +1,423 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "GpuMctpVdm.hpp"
+#include "OcpMctpVdm.hpp"
+
+#include <endian.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+TEST(PackMessage, goodPathTest)
+{
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    hdr.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    hdr.instance_id = 0;
+    hdr.msg_type = 0x04;
+
+    uint16_t pciVendorId{0x10de};
+
+    ocp::accelerator_management::BindingPciVid msg{};
+
+    auto rc = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    EXPECT_EQ(msg.pci_vendor_id, htobe16(pciVendorId));
+    EXPECT_EQ(msg.reserved, 0);
+    EXPECT_EQ(msg.datagram, 0);
+    EXPECT_EQ(msg.request, 1);
+    EXPECT_EQ(msg.ocp_type, ocp::accelerator_management::type);
+    EXPECT_EQ(msg.ocp_version, ocp::accelerator_management::version);
+    EXPECT_EQ(msg.ocp_accelerator_management_msg_type, hdr.msg_type);
+    EXPECT_EQ(msg.instance_id, hdr.instance_id);
+}
+
+TEST(PackMessage, badPathTest)
+{
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    uint16_t pciVendorId{};
+
+    // Message pointer is NULL test is no longer valid with references
+    // However, we'll leave this test block for completeness
+
+    // Instance ID out of range
+    ocp::accelerator_management::BindingPciVid msg{};
+    hdr.ocp_accelerator_management_msg_type =
+        static_cast<uint8_t>(ocp::accelerator_management::MessageType::REQUEST);
+    hdr.instance_id = 32;
+    auto rc = ocp::accelerator_management::packHeader(pciVendorId, hdr, msg);
+    EXPECT_EQ(rc,
+              ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA);
+}
+
+TEST(encodeReasonCode, testGoodEncodeReasonCode)
+{
+    std::vector<uint8_t> responseMsg(
+        sizeof(ocp::accelerator_management::BindingPciVid) +
+        sizeof(ocp::accelerator_management::CommonNonSuccessResponse));
+    auto* response = new (responseMsg.data())
+        ocp::accelerator_management::Message;
+
+    uint8_t cc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    uint16_t reasonCode = static_cast<uint16_t>(
+        ocp::accelerator_management::ReasonCode::REASON_NONE);
+
+    auto rc = ocp::accelerator_management::encodeReasonCode(
+        cc, reasonCode, 0x00, *response);
+
+    ocp::accelerator_management::CommonNonSuccessResponse resp{};
+    std::memcpy(&resp, &response->data, sizeof(resp));
+
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(static_cast<uint8_t>(
+                  ocp::accelerator_management::CompletionCode::ERROR),
+              resp.completion_code);
+    EXPECT_EQ(0x00, resp.command);
+    EXPECT_EQ(static_cast<uint16_t>(
+                  ocp::accelerator_management::ReasonCode::REASON_NONE),
+              le16toh(resp.reason_code));
+}
+
+TEST(encodeReasonCode, testBadEncodeReasonCode)
+{
+    // We cannot test null pointer with references
+    // This test is no longer applicable
+}
+
+TEST(decodeReasonCodeCC, testGoodDecodeReasonCode)
+{
+    std::vector<uint8_t> responseMsg{
+        0x10,
+        0xDE, // PCI VID
+        0x00, // RQ=0, D=0, RSVD=0, INSTANCE_ID=0
+        0x89, // OCP_TYPE=8, OCP_VER=9
+        0x00, // MSG_TYPE
+        0x09, // command
+        0x01, // completion code !=
+              // ocp::accelerator_management::CompletionCode::SUCCESS
+        0x00, // reason code
+        0x00};
+
+    auto* response = new (responseMsg.data())
+        ocp::accelerator_management::Message;
+    size_t msgLen = responseMsg.size();
+
+    uint8_t cc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    uint16_t reasonCode = static_cast<uint16_t>(
+        ocp::accelerator_management::ReasonCode::REASON_NONE);
+
+    auto rc = ocp::accelerator_management::decodeReasonCodeAndCC(
+        *response, msgLen, cc, reasonCode);
+
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(cc, static_cast<uint8_t>(
+                      ocp::accelerator_management::CompletionCode::ERROR));
+    EXPECT_EQ(reasonCode, 0x0000);
+}
+
+TEST(decodeReasonCodeCC, testGoodDecodeCompletionCode)
+{
+    std::vector<uint8_t> responseMsg{
+        0x10,
+        0xDE, // PCI VID
+        0x00, // RQ=0, D=0, RSVD=0, INSTANCE_ID=0
+        0x89, // OCP_TYPE=8, OCP_VER=9
+        0x00, // MSG_TYPE
+        0x09, // command
+        0x00, // completion code =
+              // ocp::accelerator_management::CompletionCode::SUCCESS
+        0x00, // reason code
+        0x02};
+
+    auto* response = new (responseMsg.data())
+        ocp::accelerator_management::Message;
+    size_t msgLen = responseMsg.size();
+
+    uint8_t cc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    uint16_t reasonCode = static_cast<uint16_t>(
+        ocp::accelerator_management::ReasonCode::REASON_NONE);
+
+    auto rc = ocp::accelerator_management::decodeReasonCodeAndCC(
+        *response, msgLen, cc, reasonCode);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(cc, static_cast<uint8_t>(
+                      ocp::accelerator_management::CompletionCode::SUCCESS));
+    EXPECT_EQ(reasonCode,
+              static_cast<uint16_t>(
+                  ocp::accelerator_management::ReasonCode::REASON_NONE));
+}
+
+TEST(decodeReasonCode, testBadDecodeReasonCode)
+{
+    std::vector<uint8_t> responseMsg{
+        0x10,
+        0xDE, // PCI VID
+        0x00, // RQ=0, D=0, RSVD=0, INSTANCE_ID=0
+        0x89, // OCP_TYPE=8, OCP_VER=9
+        0x00, // MSG_TYPE
+        0x09, // command
+        0x01, // completion code
+        0x00, // reason code
+        0x00};
+
+    auto* response = new (responseMsg.data())
+        ocp::accelerator_management::Message;
+    size_t msgLen = responseMsg.size();
+
+    uint8_t cc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    uint16_t reasonCode = static_cast<uint16_t>(
+        ocp::accelerator_management::ReasonCode::REASON_NONE);
+
+    // Null pointer tests are no longer applicable with references
+
+    auto rc = ocp::accelerator_management::decodeReasonCodeAndCC(
+        *response, msgLen - 2, cc,
+        reasonCode); // sending msg len less then expected
+    EXPECT_EQ(
+        rc,
+        ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA_LENGTH);
+}
+
+TEST(GpuCommonPackTest, PackHeader)
+{
+    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 = 0x04;
+    hdr.msg_type = 0x03;
+
+    auto rc = gpu::packHeader(hdr, msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(msg.ocp_version, ocp::accelerator_management::version);
+
+    // Null pointer test is no longer applicable with references
+
+    // Instance ID out of range
+    hdr.instance_id = 32;
+    rc = gpu::packHeader(hdr, msg);
+    EXPECT_EQ(rc,
+              ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA);
+}
+
+class GpuCommonTest : public ::testing::Test
+{
+  protected:
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::Message* msg{};
+    std::vector<uint8_t> buf;
+    uint8_t instance_id{};
+    uint8_t type{};
+    uint8_t command{};
+    uint8_t cc{};
+    uint16_t reason_code{};
+    uint16_t data_size{};
+    size_t msg_len{};
+    uint16_t pci_vendor_id = gpu::nvidiaPciVendorId;
+
+    void SetUp() override
+    {
+        buf.resize(1024, 0);
+        msg_len = buf.size();
+        msg = new (buf.data()) ocp::accelerator_management::Message;
+    }
+
+    void setOcpVersionAndVendorId()
+    {
+        msg->hdr.ocp_type = ocp::accelerator_management::type;
+        msg->hdr.ocp_version = ocp::accelerator_management::version;
+        msg->hdr.pci_vendor_id = be16toh(gpu::nvidiaPciVendorId);
+    }
+
+    void changeVendorId()
+    {
+        msg->hdr.pci_vendor_id = 0x1234;
+    }
+};
+
+TEST_F(GpuCommonTest, EncodeReasonCode)
+{
+    auto rc = gpu::encodeReasonCode(cc, reason_code, command, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+}
+
+TEST_F(GpuCommonTest, DecodeReasonCode)
+{
+    ocp::accelerator_management::CompletionCode rc{};
+
+    setOcpVersionAndVendorId();
+    rc = gpu::decodeReasonCodeAndCC(*msg, msg_len, cc, reason_code);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    changeVendorId();
+    rc = gpu::decodeReasonCodeAndCC(*msg, msg_len, cc, reason_code);
+    EXPECT_EQ(rc,
+              ocp::accelerator_management::CompletionCode::ERR_INVALID_DATA);
+}
+
+class GpuSensorsTest : public ::testing::Test
+{
+  protected:
+    ocp::accelerator_management::BindingPciVidInfo hdr{};
+    ocp::accelerator_management::Message* msg{};
+    std::vector<uint8_t> buf;
+    uint8_t instance_id = 0;
+    uint8_t device_instance = 1;
+    uint8_t device_id =
+        static_cast<uint8_t>(gpu::DeviceIdentification::DEVICE_GPU);
+    uint8_t cc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::SUCCESS);
+    uint16_t reason_code = static_cast<uint16_t>(
+        ocp::accelerator_management::ReasonCode::REASON_NONE);
+    uint8_t sensor_id = 0;
+    double temperature = 25.5;
+    size_t msg_len{};
+
+    void SetUp() override
+    {
+        buf.resize(1024, 0);
+        msg = new (buf.data()) ocp::accelerator_management::Message;
+        msg_len = buf.size();
+    }
+};
+
+TEST_F(GpuSensorsTest, QueryDeviceIdentificationRequestEncode)
+{
+    auto rc = gpu::encodeQueryDeviceIdentificationRequest(instance_id, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Check that header is properly set
+    EXPECT_EQ(msg->hdr.ocp_type, ocp::accelerator_management::type);
+    EXPECT_EQ(msg->hdr.ocp_version, ocp::accelerator_management::version);
+    EXPECT_EQ(msg->hdr.instance_id, instance_id);
+
+    // Check payload
+    gpu::QueryDeviceIdentificationRequest request{};
+    std::memcpy(&request, &msg->data, sizeof(request));
+
+    EXPECT_EQ(request.hdr.command,
+              static_cast<uint8_t>(gpu::DeviceCapabilityDiscoveryCommands::
+                                       QUERY_DEVICE_IDENTIFICATION));
+    EXPECT_EQ(request.hdr.data_size, 0);
+}
+
+TEST_F(GpuSensorsTest, QueryDeviceIdentificationResponseEncode)
+{
+    auto rc = gpu::encodeQueryDeviceIdentificationResponse(
+        instance_id, cc, reason_code, device_id, device_instance, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Test with error condition
+    uint8_t errorCc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    rc = gpu::encodeQueryDeviceIdentificationResponse(
+        instance_id, errorCc, reason_code, device_id, device_instance, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+}
+
+TEST_F(GpuSensorsTest, QueryDeviceIdentificationResponseDecode)
+{
+    // First encode a response
+    auto rc = gpu::encodeQueryDeviceIdentificationResponse(
+        instance_id, cc, reason_code, device_id, device_instance, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Then decode it
+    uint8_t decodedCc{};
+    uint16_t decodedReasonCode{};
+    uint8_t decodedDeviceId{};
+    uint8_t decodedDeviceInstance{};
+
+    rc = gpu::decodeQueryDeviceIdentificationResponse(
+        *msg, msg_len, decodedCc, decodedReasonCode, decodedDeviceId,
+        decodedDeviceInstance);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(decodedCc, cc);
+    EXPECT_EQ(decodedReasonCode, reason_code);
+    EXPECT_EQ(decodedDeviceId, device_id);
+    EXPECT_EQ(decodedDeviceInstance, device_instance);
+}
+
+TEST_F(GpuSensorsTest, GetTemperatureReadingRequestEncode)
+{
+    auto rc =
+        gpu::encodeGetTemperatureReadingRequest(instance_id, sensor_id, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Check that header is properly set
+    EXPECT_EQ(msg->hdr.ocp_type, ocp::accelerator_management::type);
+    EXPECT_EQ(msg->hdr.ocp_version, ocp::accelerator_management::version);
+    EXPECT_EQ(msg->hdr.instance_id, instance_id);
+
+    // Check payload
+    gpu::GetTemperatureReadingRequest request{};
+    std::memcpy(&request, &msg->data, sizeof(request));
+
+    EXPECT_EQ(request.hdr.command,
+              static_cast<uint8_t>(
+                  gpu::PlatformEnvironmentalCommands::GET_TEMPERATURE_READING));
+    EXPECT_EQ(request.hdr.data_size, sizeof(sensor_id));
+    EXPECT_EQ(request.sensor_id, sensor_id);
+}
+
+TEST_F(GpuSensorsTest, GetTemperatureReadingRequestDecode)
+{
+    // First encode a request
+    auto rc =
+        gpu::encodeGetTemperatureReadingRequest(instance_id, sensor_id, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Then decode it
+    uint8_t decodedSensorId = 0;
+    rc =
+        gpu::decodeGetTemperatureReadingRequest(*msg, msg_len, decodedSensorId);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(decodedSensorId, sensor_id);
+}
+
+TEST_F(GpuSensorsTest, GetTemperatureReadingResponseEncode)
+{
+    auto rc = gpu::encodeGetTemperatureReadingResponse(
+        instance_id, cc, reason_code, temperature, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Test with error condition
+    uint8_t errorCc = static_cast<uint8_t>(
+        ocp::accelerator_management::CompletionCode::ERROR);
+    rc = gpu::encodeGetTemperatureReadingResponse(
+        instance_id, errorCc, reason_code, temperature, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+}
+
+TEST_F(GpuSensorsTest, GetTemperatureReadingResponseDecode)
+{
+    // First encode a response
+    auto rc = gpu::encodeGetTemperatureReadingResponse(
+        instance_id, cc, reason_code, temperature, *msg);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+
+    // Then decode it
+    uint8_t decodedCc{};
+    uint16_t decodedReasonCode{};
+    double decodedTemperature{};
+
+    rc = gpu::decodeGetTemperatureReadingResponse(
+        *msg, msg_len, decodedCc, decodedReasonCode, decodedTemperature);
+    EXPECT_EQ(rc, ocp::accelerator_management::CompletionCode::SUCCESS);
+    EXPECT_EQ(decodedCc, cc);
+    EXPECT_EQ(decodedReasonCode, reason_code);
+    EXPECT_DOUBLE_EQ(decodedTemperature, temperature);
+}
diff --git a/src/gpu/tests/meson.build b/src/gpu/tests/meson.build
new file mode 100644
index 0000000..e823de2
--- /dev/null
+++ b/src/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(
+    'gpusensor_test',
+    executable(
+        'gpusensor_test',
+        'GpuSensorTest.cpp',
+        '../OcpMctpVdm.cpp',
+        '../GpuMctpVdm.cpp',
+        implicit_include_directories: false,
+        include_directories: gpusensor_test_include_dirs,
+        dependencies: [gtest_dep, gmock_dep],
+    ),
+    workdir: meson.current_source_dir(),
+)