nvmed: Add support for nvmed data posted to shared_mem

Added a feature option "nvme_shmem" to nvmed, enabling publication
of NVMe sensor values to shared memory segment. This is a requirement
for the PID tlBMC migration, allowing tlBMC to consume sensor data with
lower latency compared to DBus

Find more details here: go/pid-tlbmc-migration

Tested: https://paste.googleplex.com/6120153892913152

Google-Bug-Id: 434974253
Change-Id: I045e955c6d250e51f083daced0433c791b1d3ac7
Signed-off-by: Agrim Bharat <agrimbharat@google.com>
diff --git a/meson.build b/meson.build
index 5f17f7c..95f1522 100644
--- a/meson.build
+++ b/meson.build
@@ -93,6 +93,16 @@
     uring,
 ]
 
+shared_mem_dep = declare_dependency()
+absl_status_dep = declare_dependency()
+if get_option('nvme_shmem').enabled()
+    add_project_arguments('-DNVMED_ENABLE_SHMEM', language: 'cpp')
+    add_project_arguments('-fconstexpr-ops-limit=134217728', language: 'cpp')
+
+    shared_mem_dep = dependency('shared_memory', required: true)
+    absl_status_dep = dependency('absl_status', required: true)
+endif
+
 generated_sources = []
 generated_others = []
 yaml_sources = []
diff --git a/meson_options.txt b/meson_options.txt
index 4f2e000..cab8924 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -14,4 +14,5 @@
 option('insecure-sensor-override', type : 'feature', value : 'disabled', description : 'Enables Sensor override feature without any check.',)
 option('build-type', type: 'combo', choices: ['ci', 'bitbake'], value: 'ci', yield: false)
 option('single-worker', type : 'feature', value: 'disabled', description : 'Enable single worker per i2c bus')
-option('metric-store', type : 'combo', choices : ['disabled', 'static', 'dynamic'], value : 'disabled', description : 'Metric store options. If disabled, then no metrics will be loaded; if static, then will load all metrics but will not dynamically decide which to load; if dynamic then metric store will load specific metrics dynamically based on conditions.')
\ No newline at end of file
+option('metric-store', type : 'combo', choices : ['disabled', 'static', 'dynamic'], value : 'disabled', description : 'Metric store options. If disabled, then no metrics will be loaded; if static, then will load all metrics but will not dynamically decide which to load; if dynamic then metric store will load specific metrics dynamically based on conditions.')
+option('nvme_shmem', type: 'feature', value: 'disabled', description: 'Enable shared memory for NVMe sensors (PID tlBMC migration).', )
\ No newline at end of file
diff --git a/src/NVMeUtil.hpp b/src/NVMeUtil.hpp
index 0f80f65..694c065 100644
--- a/src/NVMeUtil.hpp
+++ b/src/NVMeUtil.hpp
@@ -3,10 +3,51 @@
 #include <boost/asio.hpp>
 #include <phosphor-logging/lg2.hpp>
 
+#include <chrono>
 #include <filesystem>
+#include <mutex>
 #include <optional>
 #include <system_error>
 
+/**
+ * @brief Logs a message via lg2 at most once every N seconds specific to the
+ * call site.
+ *
+ * This macro utilizes static local variables to maintain state specific
+ * to the exact line of code where the macro is expanded.
+ *
+ * It uses double-checked locking to ensure thread safety if multiple threads
+ * hit the exact same log statement simultaneously.
+ *
+ * @param level The lg2 severity level (e.g., info, error, warning, debug).
+ * @param N The interval in seconds.
+ * @param msg The message format string.
+ * @param ... Optional arguments for the format string.
+ */
+#define LG2_LOG_EVERY_N_SEC(level, N, msg, ...)                                \
+    do                                                                         \
+    {                                                                          \
+        /* The magic happens here: These static variables are unique per */    \
+        /* every distinct place this macro is used in the source code. */      \
+        static std::chrono::steady_clock::time_point _lg2_site_last_time;      \
+        static std::mutex _lg2_site_mutex;                                     \
+                                                                               \
+        /* Use steady_clock so system time changes don't break the timer */    \
+        auto _lg2_now = std::chrono::steady_clock::now();                      \
+                                                                               \
+        /* Optimization: Quick check without locking first */                  \
+        if (_lg2_now - _lg2_site_last_time >= std::chrono::seconds(N))         \
+        {                                                                      \
+            std::lock_guard<std::mutex> _lg2_lock(_lg2_site_mutex);            \
+            /* Thread Safety: Double-check inside lock */                      \
+            if (_lg2_now - _lg2_site_last_time >= std::chrono::seconds(N))     \
+            {                                                                  \
+                lg2::level(msg, ##__VA_ARGS__);                                \
+                _lg2_site_last_time = _lg2_now;                                \
+            }                                                                  \
+        }                                                                      \
+    } while (0)
+
 namespace nvme
 {
 static constexpr const char* sensorType = "NVME1000";
@@ -192,7 +233,7 @@
     {
         return;
     }
-    timer->expires_from_now(
+    timer->expires_after(
         std::chrono::duration_cast<std::chrono::milliseconds>(delay));
     timer->async_wait(std::bind_front(detail::pollCtemp<T>, timer, delay,
                                       dataFetcher, dataProcessor));
diff --git a/src/meson.build b/src/meson.build
index 291ac62..ecbb881 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -237,6 +237,13 @@
         nvme_dbus_dep
     ]
 
+    if get_option('nvme_shmem').enabled()
+        nvme_deps += [
+            absl_status_dep,
+            shared_mem_dep
+        ]
+    endif
+
     cxx = meson.get_compiler('cpp')
 
     executable(
diff --git a/src/sensor.hpp b/src/sensor.hpp
index 07926a0..504502b 100644
--- a/src/sensor.hpp
+++ b/src/sensor.hpp
@@ -2,10 +2,12 @@
 
 #include "dbus-sensor_config.h"
 
+#include "NVMeUtil.hpp"
 #include "SensorPaths.hpp"
 #include "Thresholds.hpp"
 #include "Utils.hpp"
 
+#include <phosphor-logging/lg2.hpp>
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/exception.hpp>
 
@@ -14,6 +16,12 @@
 #include <string>
 #include <vector>
 
+#ifdef NVMED_ENABLE_SHMEM
+#include <absl/status/status.h>
+#include <absl/strings/str_cat.h>
+#include <shared_memory/static_client_impl.h>
+#endif
+
 constexpr size_t sensorFailedPollTimeMs = 5000;
 
 // Enable useful logging with sensor instrumentation
@@ -585,5 +593,32 @@
         internalSet = true;
         updateProperty(sensorInterface, value, newValue, "Value");
         internalSet = false;
+
+#ifdef NVMED_ENABLE_SHMEM
+        milotic_tlbmc::IpcClient* shmClientPtr =
+            &milotic_tlbmc::StaticSharedMemoryClient::GetInstance();
+        if (shmClientPtr != nullptr)
+        {
+            std::string sharedMemSensorName = absl::StrCat("sharedmem_",
+                                                           this->name);
+
+            absl::Status status =
+                shmClientPtr->UpdateSensorValue(sharedMemSensorName, newValue);
+
+            if (!status.ok())
+            {
+                LG2_LOG_EVERY_N_SEC(
+                    error, 20,
+                    "Failed to publish sensor value to shared memory for {SENSOR_NAME}: {STATUS}",
+                    "SENSOR_NAME", sharedMemSensorName, "STATUS",
+                    status.ToString());
+            }
+        }
+        else
+        {
+            LG2_LOG_EVERY_N_SEC(
+                error, 20, "shmClientPtr is null, cannot update shared memory");
+        }
+#endif
     }
 };