Add gpio-presence-sensor

This is to implement the gpio based hw inventory design [1].

There is a new meson option 'gpio-presence' to enable/disable the
daemon.

Summary of the functionality:

- fetch configuration from EM, according to the configuration interface
- the D-Bus interface is
  xyz.openbmc_project.Configuration.GPIODeviceDetect
- the configuration represents devices for which presence can be
  detected based on gpio values.
- watch gpios for changes
- add/remove the xyz.openbmc_project.Inventory.Source.DevicePresence
  interface on the object path based on gpio values.

References:
[1] https://github.com/openbmc/docs/blob/master/designs/inventory/gpio-based-hardware-inventory.md
[2] https://www.kernel.org/doc/html/latest/admin-guide/gpio/gpio-sim.html

Tested: using linux gpio-sim facility, see below

1. create a fake gpio via [2]
2. configure gpio-presence-sensor as per [1]
3. run the gpio-presence-sensor
4. change the value of the gpio previously configured
5. there should be log output (at debug level)
6. the dbus interfaces exposed should appear/disappear as per [1]

Change-Id: I4cf039b583247581aa5c6c6c59e7fc41ced0bb85
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/meson.build b/meson.build
index a63b6a9..ffef7c8 100644
--- a/meson.build
+++ b/meson.build
@@ -28,7 +28,7 @@
     i2c = cpp.find_library('i2c')
 endif
 
-if get_option('devicetree-vpd')
+if get_option('devicetree-vpd') or get_option('gpio-presence')
     phosphor_dbus_interfaces_dep = dependency(
         'phosphor-dbus-interfaces',
         include_type: 'system',
@@ -39,6 +39,10 @@
 sdbusplus = dependency('sdbusplus', include_type: 'system')
 phosphor_logging_dep = dependency('phosphor-logging')
 
+if get_option('gpio-presence') or get_option('tests').allowed()
+    libgpio_dep = dependency('libgpiodcxx', default_options: ['bindings=cxx'])
+endif
+
 systemd = dependency('systemd')
 systemd_system_unit_dir = systemd.get_variable(
     'systemdsystemunitdir',
@@ -182,4 +186,22 @@
             include_directories: 'src',
         ),
     )
+
+    test(
+        'test_gpio_presence',
+        executable(
+            'test_gpio_presence',
+            'test/test_gpio_presence.cpp',
+            cpp_args: test_boost_args,
+            include_directories: ['src'],
+            dependencies: [
+                boost,
+                gtest,
+                gmock,
+                phosphor_logging_dep,
+                libgpio_dep,
+            ],
+            link_with: gpio_presence_lib,
+        ),
+    )
 endif
diff --git a/meson.options b/meson.options
index 65c1f97..d658c8b 100644
--- a/meson.options
+++ b/meson.options
@@ -28,3 +28,9 @@
     value: true,
     description: 'Run JSON schema validation during the build.',
 )
+option(
+    'gpio-presence',
+    type: 'boolean',
+    value: true,
+    description: 'Build gpio presence daemon',
+)
diff --git a/service_files/meson.build b/service_files/meson.build
index 8576ba0..0bbbeea 100644
--- a/service_files/meson.build
+++ b/service_files/meson.build
@@ -2,6 +2,7 @@
     ['xyz.openbmc_project.FruDevice.service', 'fru-device'],
     ['xyz.openbmc_project.EntityManager.service', ''],
     ['devicetree-vpd-parser.service', 'devicetree-vpd'],
+    ['xyz.openbmc_project.gpiopresence.service', 'gpio-presence'],
 ]
 
 foreach u : unit_files
diff --git a/service_files/xyz.openbmc_project.gpiopresence.service b/service_files/xyz.openbmc_project.gpiopresence.service
new file mode 100644
index 0000000..0f0b2ce
--- /dev/null
+++ b/service_files/xyz.openbmc_project.gpiopresence.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=GPIO Presence Sensor
+
+[Service]
+ExecStart=/usr/libexec/entity-manager/gpio-presence-sensor
+Restart=always
+Type=dbus
+BusName=xyz.openbmc_project.gpiopresence
+
+[Install]
+WantedBy=multi-user.target
+Alias=dbus-xyz.openbmc_project.gpiopresence.service
diff --git a/src/gpio-presence/README.md b/src/gpio-presence/README.md
new file mode 100644
index 0000000..29ad1d2
--- /dev/null
+++ b/src/gpio-presence/README.md
@@ -0,0 +1,96 @@
+# gpio-presence-sensor
+
+This program was originally implemented following the design [1].
+
+## Configuration
+
+See [1] for the full design.
+
+Example EM config fragments:
+
+```
+{
+  Exposes:
+  [
+    {
+      "Name": "com.meta.Hardware.Yv4.cable0",
+      "PresencePinNames": ["presence-cable0"],
+      "PresencePinValues": [1],
+      "Type": "GPIODeviceDetect"
+    },
+    {
+      "Name": "com.meta.Hardware.Yv4.ComputeCard",
+      "PresencePinNames": ["presence-slot0a", "presence-slot0b"],
+      "PresencePinValues": [0, 1],
+      "Type": "GPIODeviceDetect"
+    },
+    {
+      "Name": "com.meta.Hardware.Yv4.SidecarExpansion",
+      "PresencePinNames": ["presence-slot0a", "presence-slot0b"],
+      "PresencePinValues": [1, 0],
+      "Type": "GPIODeviceDetect"
+    },
+    {
+      "Name": "com.meta.Hardware.Yv4.AirBlocker",
+      "PresencePinNames": ["presence-slot0a", "presence-slot0b"],
+      "PresencePinValues": [1, 1],
+      "Type": "GPIODeviceDetect"
+    },
+    {
+      "Name": "com.meta.Hardware.Yv4.fanboard0",
+      "PresencePinNames": ["presence-fanboard0"],
+      "PresencePinValues": [0],
+      "Type": "GPIODeviceDetect"
+    },
+    ...
+  ],
+  ...
+  "Name": "My Chassis",
+  "Probe": "xyz.openbmc_project.FruDevice({'BOARD_PRODUCT_NAME': 'MYBOARDPRODUCT*'})",
+  "Type": "Board",
+}
+```
+
+The above configuration can then cause a Probe match in another configuration,
+like below:
+
+```
+{
+  Exposes:
+  [
+      {
+        "Address": "0x28",
+        "Bus": 5,
+        "EntityId": 7,
+        "EntityInstance": 0,
+        "Name": "fanboard_air_inlet",
+        "Name1": "fanboard_air_outlet",
+        "Type": "NCT7802"
+    },
+    ...
+  ],
+  ...
+  "Name": "My Fan Board 0",
+  "Probe": "xyz.openbmc_project.Inventory.Source.DevicePresence({'Name': 'com.meta.Hardware.Yv4.fanboard0'})",
+  "Type": "Board",
+}
+```
+
+Notice the **xyz.openbmc_project.Inventory.Source.DevicePresence** interface.
+This is what the gpio-presence daemon exposes on dbus when the hardware is
+detected as present. The **Name** property in the Probe statement is the same as
+configured as in the first json fragment.
+
+## Applications
+
+Applications include detecting fan boards, air blockers, cables and other simple
+components for which no standard / well-defined way exists to detect them
+otherwise.
+
+It can also be used as detection redundancy in case another detection mechanism
+like FRU eeprom is corrupted or unavailable.
+
+## References
+
+- [1]
+  https://github.com/openbmc/docs/blob/master/designs/inventory/gpio-based-hardware-inventory.md
diff --git a/src/gpio-presence/config_provider.cpp b/src/gpio-presence/config_provider.cpp
new file mode 100644
index 0000000..7948802
--- /dev/null
+++ b/src/gpio-presence/config_provider.cpp
@@ -0,0 +1,149 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024. All rights
+ * reserved. SPDX-License-Identifier: Apache-2.0
+ */
+#include "config_provider.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/match.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+#include <ranges>
+#include <string>
+
+PHOSPHOR_LOG2_USING;
+
+using VariantType =
+    std::variant<std::vector<std::string>, std::string, int64_t, uint64_t,
+                 double, int32_t, uint32_t, int16_t, uint16_t, uint8_t, bool>;
+using ConfigMap = boost::container::flat_map<std::string, VariantType>;
+using ConfigData = boost::container::flat_map<std::string, ConfigMap>;
+
+namespace gpio_presence
+{
+
+ConfigProvider::ConfigProvider(sdbusplus::async::context& ctx,
+                               const std::string& interface) :
+    interface(interface), ctx(ctx)
+{}
+
+auto ConfigProvider::initialize(AddedCallback addConfig,
+                                RemovedCallback removeConfig)
+    -> sdbusplus::async::task<void>
+{
+    ctx.spawn(handleInterfacesAdded(addConfig));
+    ctx.spawn(handleInterfacesRemoved(removeConfig));
+
+    co_await getConfig(addConfig);
+}
+
+auto ConfigProvider::getConfig(AddedCallback addConfig)
+    -> sdbusplus::async::task<void>
+{
+    auto client = sdbusplus::client::xyz::openbmc_project::ObjectMapper<>(ctx)
+                      .service("xyz.openbmc_project.ObjectMapper")
+                      .path("/xyz/openbmc_project/object_mapper");
+
+    debug("calling 'GetSubTree' to find instances of {INTF}", "INTF",
+          interface);
+
+    using SubTreeType =
+        std::map<std::string, std::map<std::string, std::vector<std::string>>>;
+    SubTreeType res = {};
+
+    try
+    {
+        std::vector<std::string> interfaces = {interface};
+        res = co_await client.get_sub_tree("/xyz/openbmc_project/inventory", 0,
+                                           interfaces);
+    }
+    catch (std::exception& e)
+    {
+        error("Failed GetSubTree call for configuration interface: {ERR}",
+              "ERR", e);
+    }
+
+    if (res.empty())
+    {
+        co_return;
+    }
+
+    // call the user callback for all the device that is already available
+    for (auto& [path, serviceInterfaceMap] : res)
+    {
+        for (const auto& service :
+             std::ranges::views::keys(serviceInterfaceMap))
+        {
+            debug("found configuration interface at {SERVICE} {PATH} {INTF}",
+                  "SERVICE", service, "PATH", path, "INTF", interface);
+
+            addConfig(path);
+        }
+    }
+}
+
+namespace rules_intf = sdbusplus::bus::match::rules;
+
+const auto senderRule = rules_intf::sender("xyz.openbmc_project.EntityManager");
+
+auto ConfigProvider::handleInterfacesAdded(AddedCallback addConfig)
+    -> sdbusplus::async::task<void>
+{
+    debug("setting up dbus match for interfaces added");
+
+    sdbusplus::async::match addedMatch(
+        ctx, rules_intf::interfacesAdded() + senderRule);
+
+    while (!ctx.stop_requested())
+    {
+        auto [objPath, intfMap] =
+            co_await addedMatch
+                .next<sdbusplus::message::object_path, ConfigData>();
+
+        debug("Detected interface added on {OBJPATH}", "OBJPATH", objPath);
+
+        if (!std::ranges::contains(std::views::keys(intfMap), interface))
+        {
+            continue;
+        }
+
+        try
+        {
+            addConfig(objPath);
+        }
+        catch (std::exception& e)
+        {
+            error("Incomplete or invalid config found: {ERR}", "ERR", e);
+        }
+    }
+};
+
+auto ConfigProvider::handleInterfacesRemoved(RemovedCallback removeConfig)
+    -> sdbusplus::async::task<void>
+{
+    debug("setting up dbus match for interfaces removed");
+
+    sdbusplus::async::match removedMatch(
+        ctx, rules_intf::interfacesRemoved() + senderRule);
+
+    while (!ctx.stop_requested())
+    {
+        auto [objectPath, interfaces] =
+            co_await removedMatch.next<sdbusplus::message::object_path,
+                                       std::vector<std::string>>();
+
+        if (!std::ranges::contains(interfaces, interface))
+        {
+            continue;
+        }
+
+        debug("Detected interface {INTF} removed on {OBJPATH}", "INTF",
+              interface, "OBJPATH", objectPath);
+
+        removeConfig(objectPath);
+    }
+};
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/config_provider.hpp b/src/gpio-presence/config_provider.hpp
new file mode 100644
index 0000000..4bacfc7
--- /dev/null
+++ b/src/gpio-presence/config_provider.hpp
@@ -0,0 +1,43 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024. All rights
+ * reserved. SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/async/task.hpp>
+
+namespace gpio_presence
+{
+
+using AddedCallback =
+    std::function<void(const sdbusplus::message::object_path&)>;
+
+using RemovedCallback =
+    std::function<void(const sdbusplus::message::object_path&)>;
+
+class ConfigProvider
+{
+  public:
+    explicit ConfigProvider(sdbusplus::async::context& ctx,
+                            const std::string& interface);
+
+    auto initialize(AddedCallback addConfig, RemovedCallback removeConfig)
+        -> sdbusplus::async::task<void>;
+
+  private:
+    auto getConfig(AddedCallback addConfig) -> sdbusplus::async::task<void>;
+
+    auto handleInterfacesAdded(AddedCallback addConfig)
+        -> sdbusplus::async::task<void>;
+
+    auto handleInterfacesRemoved(RemovedCallback removeConfig)
+        -> sdbusplus::async::task<void>;
+
+    // name of the dbus configuration interface
+    std::string interface;
+
+    sdbusplus::async::context& ctx;
+};
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/device_presence.cpp b/src/gpio-presence/device_presence.cpp
new file mode 100644
index 0000000..44d6e6a
--- /dev/null
+++ b/src/gpio-presence/device_presence.cpp
@@ -0,0 +1,110 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024. All rights
+ * reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "device_presence.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/Configuration/GPIODeviceDetect/client.hpp>
+#include <xyz/openbmc_project/Configuration/GPIODeviceDetect/common.hpp>
+#include <xyz/openbmc_project/Inventory/Source/DevicePresence/aserver.hpp>
+
+#include <string>
+#include <vector>
+
+PHOSPHOR_LOG2_USING;
+
+namespace gpio_presence
+{
+
+DevicePresence::DevicePresence(
+    sdbusplus::async::context& ctx, const std::vector<std::string>& gpioNames,
+    const std::vector<uint64_t>& gpioValues, const std::string& deviceName,
+    const std::unordered_map<std::string, bool>& gpioState) :
+    deviceName(deviceName), gpioState(gpioState), ctx(ctx)
+{
+    for (size_t i = 0; i < gpioNames.size(); i++)
+    {
+        GPIO_POLARITY polarity =
+            (gpioValues[i] == 0) ? ACTIVE_LOW : ACTIVE_HIGH;
+        gpioPolarity[gpioNames[i]] = polarity;
+    }
+}
+
+auto DevicePresence::updateGPIOPresence(const std::string& gpioLine) -> void
+{
+    if (!gpioPolarity.contains(gpioLine))
+    {
+        return;
+    }
+
+    updateDbusInterfaces();
+}
+
+auto DevicePresence::getObjPath() const -> sdbusplus::message::object_path
+{
+    sdbusplus::message::object_path objPathBase(
+        "/xyz/openbmc_project/GPIODeviceDetected/");
+    sdbusplus::message::object_path objPath = objPathBase / deviceName;
+    return objPath;
+}
+
+auto DevicePresence::isPresent() -> bool
+{
+    for (auto& [name, polarity] : gpioPolarity)
+    {
+        if (!gpioState.contains(name))
+        {
+            error("GPIO {NAME} not in cached state", "NAME", name);
+            return false;
+        }
+
+        const bool state = gpioState.at(name);
+
+        if (state && polarity == ACTIVE_LOW)
+        {
+            return false;
+        }
+        if (!state && polarity == ACTIVE_HIGH)
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+auto DevicePresence::updateDbusInterfaces() -> void
+{
+    debug("Updating dbus interface for config {OBJPATH}", "OBJPATH",
+          deviceName);
+
+    const bool present = isPresent();
+    sdbusplus::message::object_path objPath = getObjPath();
+
+    if (present && !detectedIface)
+    {
+        info("Detected {NAME} as present, adding dbus interface", "NAME",
+             deviceName);
+
+        detectedIface =
+            std::make_unique<DevicePresenceInterface>(ctx, objPath.str.c_str());
+
+        detectedIface->name(deviceName);
+
+        detectedIface->emit_added();
+    }
+
+    if (!present && detectedIface)
+    {
+        info("Detected {NAME} as absent, removing dbus interface", "NAME",
+             deviceName);
+        detectedIface->emit_removed();
+
+        detectedIface.reset();
+    }
+}
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/device_presence.hpp b/src/gpio-presence/device_presence.hpp
new file mode 100644
index 0000000..cf1a283
--- /dev/null
+++ b/src/gpio-presence/device_presence.hpp
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024. All rights
+ * reserved. SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <xyz/openbmc_project/Inventory/Source/DevicePresence/aserver.hpp>
+
+#include <string>
+
+namespace gpio_presence
+{
+
+enum GPIO_POLARITY
+{
+    ACTIVE_HIGH,
+    ACTIVE_LOW,
+};
+
+class DevicePresence;
+
+using DevicePresenceInterface =
+    sdbusplus::aserver::xyz::openbmc_project::inventory::source::DevicePresence<
+        DevicePresence>;
+
+class DevicePresence
+{
+  public:
+    DevicePresence(sdbusplus::async::context& ctx,
+                   const std::vector<std::string>& gpioNames,
+                   const std::vector<uint64_t>& gpioValues,
+                   const std::string& deviceName,
+                   const std::unordered_map<std::string, bool>& gpioState);
+
+    auto updateGPIOPresence(const std::string& gpioLine) -> void;
+
+    // @returns the object path of the 'detected' interface
+    auto getObjPath() const -> sdbusplus::message::object_path;
+
+    // computed from the state of the configured gpios
+    auto isPresent() -> bool;
+
+    // name of the device to detect, e.g. 'cable0'
+    // (taken from EM config)
+    const std::string deviceName;
+
+    // maps the name of the gpio to its polarity
+    std::map<std::string, GPIO_POLARITY> gpioPolarity;
+
+  private:
+    // reference to the map in presence manager
+    const std::unordered_map<std::string, bool>& gpioState;
+
+    sdbusplus::async::context& ctx;
+
+    auto updateDbusInterfaces() -> void;
+
+    // property added when the hw is detected
+    std::unique_ptr<DevicePresenceInterface> detectedIface = nullptr;
+};
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/gpio_presence_manager.cpp b/src/gpio-presence/gpio_presence_manager.cpp
new file mode 100644
index 0000000..856408e
--- /dev/null
+++ b/src/gpio-presence/gpio_presence_manager.cpp
@@ -0,0 +1,260 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024.
+ * All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "gpio_presence_manager.hpp"
+
+#include "device_presence.hpp"
+
+#include <boost/asio/posix/stream_descriptor.hpp>
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/timer.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/Configuration/GPIODeviceDetect/client.hpp>
+#include <xyz/openbmc_project/Configuration/GPIODeviceDetect/common.hpp>
+
+#include <memory>
+#include <ranges>
+#include <string>
+#include <utility>
+
+PHOSPHOR_LOG2_USING;
+
+namespace gpio_presence
+{
+
+GPIOPresenceManager::GPIOPresenceManager(sdbusplus::async::context& ctx) :
+    ctx(ctx), manager(ctx, "/"),
+    configProvider(
+        ConfigProvider(ctx, sdbusplus::common::xyz::openbmc_project::
+                                configuration::GPIODeviceDetect::interface))
+{}
+
+auto GPIOPresenceManager::start() -> void
+{
+    ctx.spawn(initialize());
+}
+
+auto GPIOPresenceManager::getPresence(const std::string& name) -> bool
+{
+    if (!presenceMap.contains(name))
+    {
+        return false;
+    }
+    return presenceMap.at(name)->isPresent();
+}
+
+auto GPIOPresenceManager::initialize() -> sdbusplus::async::task<void>
+{
+    co_await configProvider.initialize(
+        std::bind_front(&GPIOPresenceManager::addConfigHandler, this),
+        std::bind_front(&GPIOPresenceManager::removeConfig, this));
+}
+
+auto GPIOPresenceManager::setupBusName() const -> std::string
+{
+    debug("requesting dbus name {NAME}", "NAME", service);
+
+    ctx.request_name(service);
+    return service;
+}
+
+auto GPIOPresenceManager::addConfig(const sdbusplus::message::object_path& obj,
+                                    std::unique_ptr<DevicePresence> config)
+    -> void
+{
+    debug("adding configuration for {NAME}", "NAME", obj);
+    presenceMap.insert_or_assign(obj, std::move(config));
+
+    debug("found valid configuration at object path {OBJPATH}", "OBJPATH", obj);
+
+    auto gpioConfigs = presenceMap[obj]->gpioPolarity;
+
+    // populate fdios
+    for (auto& [gpioName, _] : gpioConfigs)
+    {
+        if (gpioLines.contains(gpioName))
+        {
+            continue;
+        }
+
+        try
+        {
+            gpioLines[gpioName] = gpiod::find_line(gpioName);
+        }
+        catch (std::exception& e)
+        {
+            error("gpiod::find_line failed: {ERROR}", "ERROR", e);
+            return;
+        }
+
+        gpiod::line_request lineConfig;
+        lineConfig.consumer = "gpio-presence";
+        lineConfig.request_type = gpiod::line_request::EVENT_BOTH_EDGES |
+                                  gpiod::line_request::DIRECTION_INPUT;
+
+        int lineFd = -1;
+        try
+        {
+            gpioLines[gpioName].request(lineConfig);
+
+            lineFd = gpioLines[gpioName].event_get_fd();
+        }
+        catch (std::exception& e)
+        {
+            error("{ERROR}", "ERROR", e);
+            return;
+        }
+        if (lineFd < 0)
+        {
+            error("could not get event fd for gpio '{NAME}'", "NAME", gpioName);
+            return;
+        }
+
+        if (!fdios.contains(gpioName))
+        {
+            fdios.insert(
+                {gpioName,
+                 std::make_unique<sdbusplus::async::fdio>(ctx, lineFd)});
+
+            ctx.spawn(readGPIOAsyncEvent(gpioName));
+        }
+    }
+}
+
+auto GPIOPresenceManager::addConfigHandler(sdbusplus::message::object_path obj)
+    -> void
+{
+    // NOLINTBEGIN(performance-unnecessary-value-param)
+    ctx.spawn(addConfigFromDbusAsync(obj));
+    // NOLINTEND(performance-unnecessary-value-param)
+}
+
+// NOLINTBEGIN(performance-unnecessary-value-param)
+auto GPIOPresenceManager::addConfigFromDbusAsync(
+    const sdbusplus::message::object_path obj) -> sdbusplus::async::task<void>
+// NOLINTEND(performance-unnecessary-value-param)
+{
+    auto props = co_await sdbusplus::client::xyz::openbmc_project::
+                     configuration::GPIODeviceDetect<>(ctx)
+                         .service("xyz.openbmc_project.EntityManager")
+                         .path(obj.str)
+                         .properties();
+
+    if (props.presence_pin_names.size() != props.presence_pin_values.size())
+    {
+        error(
+            "presence pin names and presence pin values have different sizes");
+        co_return;
+    }
+
+    auto devicePresence = std::make_unique<DevicePresence>(
+        ctx, props.presence_pin_names, props.presence_pin_values, props.name,
+        gpioState);
+
+    if (devicePresence)
+    {
+        addConfig(obj, std::move(devicePresence));
+    }
+}
+
+auto GPIOPresenceManager::removeConfig(const std::string& objPath) -> void
+{
+    if (!presenceMap.contains(objPath))
+    {
+        return;
+    }
+
+    debug("erasing configuration for object path {OBJPATH}", "OBJPATH",
+          objPath);
+    presenceMap.erase(objPath);
+
+    std::set<std::string> gpiosNeeded;
+
+    for (const auto& config : std::views::values(presenceMap))
+    {
+        for (const auto& gpio : std::views::keys(config->gpioPolarity))
+        {
+            gpiosNeeded.insert(gpio);
+        }
+    }
+
+    auto ks = std::views::keys(gpioLines);
+    std::set<std::string> trackedGPIOs{ks.begin(), ks.end()};
+
+    for (const auto& trackedGPIO : trackedGPIOs)
+    {
+        if (gpiosNeeded.contains(trackedGPIO))
+        {
+            continue;
+        }
+
+        gpioLines[trackedGPIO].release();
+
+        gpioLines.erase(trackedGPIO);
+        fdios.erase(fdios.find(trackedGPIO));
+    }
+}
+
+auto GPIOPresenceManager::updatePresence(const std::string& gpioLine,
+                                         bool state) -> void
+{
+    gpioState.insert_or_assign(gpioLine, state);
+
+    debug("GPIO line {GPIO_NAME} went {GPIO_LEVEL}", "GPIO_NAME", gpioLine,
+          "GPIO_LEVEL", (state) ? "high" : "low");
+
+    for (const auto& config : std::views::values(presenceMap))
+    {
+        config->updateGPIOPresence(gpioLine);
+    }
+}
+
+auto GPIOPresenceManager::readGPIOAsyncEvent(std::string gpioLine)
+    -> sdbusplus::async::task<void>
+{
+    debug("Watching gpio events for {LINENAME}", "LINENAME", gpioLine);
+
+    if (!fdios.contains(gpioLine))
+    {
+        error("fdio for {LINENAME} not found", "LINENAME", gpioLine);
+        co_return;
+    }
+
+    const auto& fdio = fdios[gpioLine];
+
+    try
+    {
+        const int lineValue = gpioLines[gpioLine].get_value();
+
+        updatePresence(gpioLine, lineValue == gpiod::line_event::RISING_EDGE);
+    }
+    catch (std::exception& e)
+    {
+        error("Failed to read GPIO line {LINENAME}", "LINENAME", gpioLine);
+        error("{ERROR}", "ERROR", e);
+        co_return;
+    }
+
+    while (!ctx.stop_requested())
+    {
+        co_await fdio->next();
+
+        debug("Received gpio event for {LINENAME}", "LINENAME", gpioLine);
+
+        gpioLines[gpioLine].event_read();
+
+        auto lineValue = gpioLines[gpioLine].get_value();
+
+        if (lineValue < 0)
+        {
+            error("Failed to read GPIO line {LINENAME}", "LINENAME", gpioLine);
+        }
+
+        updatePresence(gpioLine, lineValue == gpiod::line_event::RISING_EDGE);
+    }
+}
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/gpio_presence_manager.hpp b/src/gpio-presence/gpio_presence_manager.hpp
new file mode 100644
index 0000000..d490fe3
--- /dev/null
+++ b/src/gpio-presence/gpio_presence_manager.hpp
@@ -0,0 +1,92 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024. All rights
+ * reserved. SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include "config_provider.hpp"
+#include "device_presence.hpp"
+#include "sdbusplus/async/fdio.hpp"
+
+#include <gpiod.hpp>
+
+#include <string>
+#include <unordered_map>
+
+namespace gpio_presence
+{
+
+constexpr auto service = "xyz.openbmc_project.gpiopresence";
+
+class GPIOPresenceManager
+{
+  public:
+    explicit GPIOPresenceManager(sdbusplus::async::context& ctx);
+
+    // spawns the initialization function
+    auto start() -> void;
+
+    // @param[in] name       name of device, e.g. 'cable0'
+    // @returns              true if present
+    auto getPresence(const std::string& name) -> bool;
+
+    // request our dbus name
+    // @returns         our dbus name
+    auto setupBusName() const -> std::string;
+
+    // add the configuration for object at path 'obj'
+    // @param[in] obj       object path for the new config
+    // @param[in] config    configuration for the new object
+    auto addConfig(const sdbusplus::message::object_path& obj,
+                   std::unique_ptr<DevicePresence> config) -> void;
+
+    // update presence information based on new gpio state
+    // @param[in] gpioLine       name of the gpio line
+    // @param[in] state          new state of the gpio line
+    auto updatePresence(const std::string& gpioLine, bool state) -> void;
+
+    // maps gpio names to cached gpio state
+    // true <=> High
+    std::unordered_map<std::string, bool> gpioState;
+
+  private:
+    // fetch our configuration from dbus
+    // @param[in] obj object path of the configuration
+    auto addConfigFromDbusAsync(sdbusplus::message::object_path obj)
+        -> sdbusplus::async::task<void>;
+
+    // delete our configuration for the object at 'objPath'
+    // @param[in] objPath         path of the object we want to forget
+    auto removeConfig(const std::string& objPath) -> void;
+
+    // fetch configuration from dbus via object mapper
+    // and register dbus matches for configuration
+    auto initialize() -> sdbusplus::async::task<void>;
+
+    // handle config interface added
+    // @param[in] obj    object path of the configuration
+    auto addConfigHandler(sdbusplus::message::object_path obj) -> void;
+
+    // async block on fdio gpio event and handle it
+    // @param[in] gpioLine     name of the gpio to watch for events
+    auto readGPIOAsyncEvent(std::string gpioLine)
+        -> sdbusplus::async::task<void>;
+
+    // maps dbus object paths to configurations
+    // e.g. /xyz/openbmc_project/inventory/system/board/My_Baseboard/cable0
+    std::unordered_map<std::string, std::unique_ptr<DevicePresence>>
+        presenceMap;
+
+    // maps gpio names to fdios
+    std::unordered_map<std::string, std::unique_ptr<sdbusplus::async::fdio>>
+        fdios;
+    // maps gpio names to libgpiod lines
+    std::unordered_map<std::string, ::gpiod::line> gpioLines;
+
+    sdbusplus::async::context& ctx;
+    sdbusplus::server::manager_t manager;
+
+    ConfigProvider configProvider;
+};
+
+} // namespace gpio_presence
diff --git a/src/gpio-presence/main.cpp b/src/gpio-presence/main.cpp
new file mode 100644
index 0000000..c680210
--- /dev/null
+++ b/src/gpio-presence/main.cpp
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2022-2024.
+ * All rights reserved. SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "gpio_presence_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+
+using namespace gpio_presence;
+
+auto main() -> int
+{
+    lg2::debug("starting GPIO Presence Sensor");
+
+    sdbusplus::async::context ctx;
+
+    gpio_presence::GPIOPresenceManager controller(ctx);
+
+    controller.setupBusName();
+
+    controller.start();
+
+    ctx.run();
+}
diff --git a/src/gpio-presence/meson.build b/src/gpio-presence/meson.build
new file mode 100644
index 0000000..0e2c0f4
--- /dev/null
+++ b/src/gpio-presence/meson.build
@@ -0,0 +1,31 @@
+gpio_presence_lib = static_library(
+    'gpio_presence_lib',
+    'device_presence.cpp',
+    'device_presence.hpp',
+    'gpio_presence_manager.cpp',
+    'gpio_presence_manager.hpp',
+    'config_provider.cpp',
+    'config_provider.hpp',
+    dependencies: [
+        boost,
+        phosphor_logging_dep,
+        phosphor_dbus_interfaces_dep,
+        sdbusplus,
+        libgpio_dep,
+    ],
+)
+
+executable(
+    'gpio-presence-sensor',
+    'main.cpp',
+    dependencies: [
+        boost,
+        phosphor_logging_dep,
+        phosphor_dbus_interfaces_dep,
+        sdbusplus,
+        libgpio_dep,
+    ],
+    install: true,
+    install_dir: installdir,
+    link_with: gpio_presence_lib,
+)
diff --git a/src/meson.build b/src/meson.build
index 2f98faf..2187228 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -11,3 +11,7 @@
 if get_option('devicetree-vpd')
     subdir('devicetree_vpd_parser')
 endif
+
+if get_option('gpio-presence')
+    subdir('gpio-presence')
+endif
diff --git a/subprojects/libgpiod.wrap b/subprojects/libgpiod.wrap
new file mode 100644
index 0000000..e85aa49
--- /dev/null
+++ b/subprojects/libgpiod.wrap
@@ -0,0 +1,12 @@
+[wrap-file]
+directory = libgpiod-1.6.3
+source_url = https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/libgpiod-1.6.3.tar.gz
+source_filename = libgpiod-1.6.3.tar.gz
+source_hash = eb446070be1444fd7d32d32bbca53c2f3bbb0a21193db86198cf6050b7a28441
+patch_filename = libgpiod_1.6.3-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/libgpiod_1.6.3-1/get_patch
+patch_hash = 76821c637073679a88f77593c6f7ce65b4b5abf8c998f823fffa13918c8761df
+
+[provide]
+libgpiod = gpiod_dep
+libgpiodcxx = gpiodcxx_dep
diff --git a/test/test_gpio_presence.cpp b/test/test_gpio_presence.cpp
new file mode 100644
index 0000000..727bc40
--- /dev/null
+++ b/test/test_gpio_presence.cpp
@@ -0,0 +1,160 @@
+#include "gpio-presence/device_presence.hpp"
+#include "gpio-presence/gpio_presence_manager.hpp"
+
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Inventory/Source/DevicePresence/client.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace gpio_presence;
+
+auto requestStop(sdbusplus::async::context& io) -> sdbusplus::async::task<>
+{
+    io.request_stop();
+    co_return;
+}
+
+TEST(GpioPresence, ConstructionSucceeds)
+{
+    sdbusplus::async::context ctx;
+
+    gpio_presence::GPIOPresenceManager s(ctx);
+
+    ctx.spawn(requestStop(ctx));
+    ctx.run();
+}
+
+TEST(GpioPresence, AcceptConfig1Gpio)
+{
+    sdbusplus::async::context ctx;
+
+    gpio_presence::GPIOPresenceManager sensor(ctx);
+
+    std::string name = "cable0";
+    std::string gpioName = "TEST_GPIO";
+
+    std::vector<std::string> gpioNames = {gpioName};
+    std::vector<uint64_t> gpioValues = {0};
+
+    auto c = std::make_unique<gpio_presence::DevicePresence>(
+        ctx, gpioNames, gpioValues, name, sensor.gpioState);
+
+    sensor.addConfig(name, std::move(c));
+
+    sensor.updatePresence(gpioName, false);
+
+    EXPECT_EQ(sensor.getPresence(name), true);
+
+    sensor.updatePresence(gpioName, true);
+
+    EXPECT_EQ(sensor.getPresence(name), false);
+
+    ctx.spawn(requestStop(ctx));
+    ctx.run();
+}
+
+auto testDevicePresentDbus(sdbusplus::async::context& ctx)
+    -> sdbusplus::async::task<>
+{
+    gpio_presence::GPIOPresenceManager sensor(ctx);
+
+    std::string busName = sensor.setupBusName();
+
+    std::string name = "cable0";
+    std::string gpioName = "TEST_GPIO";
+
+    std::vector<std::string> gpioNames = {gpioName};
+    std::vector<uint64_t> gpioValues = {0};
+
+    auto c = std::make_unique<gpio_presence::DevicePresence>(
+        ctx, gpioNames, gpioValues, name, sensor.gpioState);
+
+    sdbusplus::message::object_path objPath = c->getObjPath();
+
+    sensor.addConfig(name, std::move(c));
+
+    sensor.updatePresence(gpioName, false);
+
+    lg2::debug("found obj path {OBJPATH}", "OBJPATH", objPath);
+
+    auto client = sdbusplus::client::xyz::openbmc_project::inventory::source::
+                      DevicePresence<>(ctx)
+                          .service(busName)
+                          .path(objPath.str);
+
+    std::string nameFound = co_await client.name();
+
+    assert(nameFound == "cable0");
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(GpioPresence, DevicePresentDbus)
+{
+    sdbusplus::async::context ctx;
+    ctx.spawn(testDevicePresentDbus(ctx));
+    ctx.run();
+}
+
+auto testDevicePresentThenDisappearDbus(sdbusplus::async::context& ctx)
+    -> sdbusplus::async::task<>
+{
+    gpio_presence::GPIOPresenceManager sensor(ctx);
+
+    std::string busName = sensor.setupBusName();
+
+    std::string name = "cable0";
+    std::string gpioName = "TEST_GPIO";
+
+    std::vector<std::string> gpioNames = {gpioName};
+    std::vector<uint64_t> gpioValues = {0};
+
+    auto c = std::make_unique<gpio_presence::DevicePresence>(
+        ctx, gpioNames, gpioValues, name, sensor.gpioState);
+
+    sdbusplus::message::object_path objPath = c->getObjPath();
+
+    sensor.addConfig(name, std::move(c));
+
+    sensor.updatePresence(gpioName, false);
+
+    lg2::debug("found obj path {OBJPATH}", "OBJPATH", objPath);
+
+    auto client = sdbusplus::client::xyz::openbmc_project::inventory::source::
+                      DevicePresence<>(ctx)
+                          .service(busName)
+                          .path(objPath.str);
+
+    std::string nameFound = co_await client.name();
+
+    assert(nameFound == "cable0");
+
+    // gpio goes high, cable 0 should disappear
+    sensor.updatePresence(gpioName, true);
+
+    try
+    {
+        co_await client.name();
+        assert(false);
+    }
+    catch (std::exception& _)
+    {
+        // expected, since cable 0 is gone.
+        // have to do something here to shut up clang-tidy
+        std::cout << "" << std::endl;
+    }
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(GpioPresence, DevicePresentThenDisappearDbus)
+{
+    sdbusplus::async::context ctx;
+    ctx.spawn(testDevicePresentThenDisappearDbus(ctx));
+    ctx.run();
+}