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();
+}