Add phosphor-multi-gpio-presence

The new service works like phosphor-gpio-presence, but uses libgpiod
and can monitor an arbitrary amount of GPIOs.

The driver loading feature hasn't been ported to the new service.

Change-Id: I412345f804208e48eec40ec020b3a0d8f668a34b
Signed-off-by: Patrick Rudolph <patrick.rudolph@9elements.com>
diff --git a/README.md b/README.md
index 7851995..c8090eb 100644
--- a/README.md
+++ b/README.md
@@ -73,3 +73,56 @@
   { "Name": "SystemReset", "GpioNum": 46, "ChipId": "0" }
 ]
 ```
+
+### `phosphor-multi-gpio-presence`
+
+This daemon accepts command line parameter as a well-defined GPIO configuration
+file in json format to monitor list of gpios from config file and sets inventory
+presence as defined in config based on gpio state change. It uses libgpiod
+library.
+
+### Difference
+
+New implementation (phosphor-multi-gpio-presence) provides multiple gpio line
+monitoring in single instance of phosphor-multi-gpio-presence running. It is
+very easy to add list of gpios into JSON config file and it also supports of
+GPIO line by name defined in kernel.
+
+## Configuration
+
+There is a phosphor-multi-gpio-presence.json file that defines details of GPIOs
+which is required to be monitored. This file can be replaced with a platform
+specific configuration file via bbappend.
+
+Following are fields in json file
+
+1. Name: PrettyName of inventory item
+2. LineName: this is the line name defined in device tree for specific gpio
+3. GpioNum: GPIO offset, this field is optional if LineName is defined.
+4. ChipId: This is device name either offset ("0") or complete gpio device
+   ("gpiochip0"). This field is not required if LineName is defined.
+5. Inventory: Object path under inventory that will be created
+6. ExtraInterfaces: [Optional] List of interfaces to associate to inventory item
+7. ActiveLow: [Optional] Object is present on LOW level
+8. Bias: [Optional] Configure a BIAS on the GPIO line, for example PULL_UP
+
+## Sample config file
+
+```json
+[
+  {
+    "Name": "DIMM A0",
+    "LineName": "POWER_BUTTON",
+    "Inventory": "/system/chassis/motherboard/dimm_a0"
+  },
+  {
+    "Name": "Powersupply 0",
+    "ChipId": "0",
+    "GpioNum": 14,
+    "Inventory": "/system/chassis/motherboard/powersupply0",
+    "ActiveLow": true,
+    "Bias": "PULL_UP",
+    "ExtraInterfaces": ["xyz.openbmc_project.Inventory.Item.PowerSupply"]
+  }
+]
+```
diff --git a/meson.build b/meson.build
index 5337496..0148a0c 100644
--- a/meson.build
+++ b/meson.build
@@ -62,6 +62,12 @@
 )
 
 fs.copyfile(
+    'phosphor-multi-gpio-presence.service',
+    install: true,
+    install_dir: systemd_system_unit_dir
+)
+
+fs.copyfile(
     'phosphor-gpio-presence@.service',
     install: true,
     install_dir: systemd_system_unit_dir
@@ -136,6 +142,7 @@
         cli11_dep,
         libgpiod,
         nlohmann_json_dep,
+        phosphor_dbus_interfaces,
         phosphor_logging,
         sdbusplus,
     ],
@@ -144,6 +151,7 @@
 )
 
 subdir('presence')
+subdir('multi-presence')
 
 build_tests = get_option('tests')
 if build_tests.allowed()
diff --git a/multi-presence/gpio_presence.cpp b/multi-presence/gpio_presence.cpp
new file mode 100644
index 0000000..338248f
--- /dev/null
+++ b/multi-presence/gpio_presence.cpp
@@ -0,0 +1,223 @@
+/**
+ * Copyright © 2019 Facebook
+ * Copyright © 2023 9elements GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "gpio_presence.hpp"
+
+#include "xyz/openbmc_project/Common/error.hpp"
+
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/elog.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/bus.hpp>
+
+namespace phosphor
+{
+namespace gpio
+{
+
+using namespace phosphor::logging;
+using namespace sdbusplus::xyz::openbmc_project::Common::Error;
+
+constexpr auto INVENTORY_PATH = "/xyz/openbmc_project/inventory";
+constexpr auto INVENTORY_INTF = "xyz.openbmc_project.Inventory.Manager";
+
+constexpr auto MAPPER_BUSNAME = "xyz.openbmc_project.ObjectMapper";
+constexpr auto MAPPER_PATH = "/xyz/openbmc_project/object_mapper";
+constexpr auto MAPPER_INTERFACE = "xyz.openbmc_project.ObjectMapper";
+
+std::string getService(const std::string& path, const std::string& interface,
+                       sdbusplus::bus_t& bus)
+{
+    auto mapperCall = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
+                                          MAPPER_INTERFACE, "GetObject");
+
+    mapperCall.append(path);
+    mapperCall.append(std::vector<std::string>({interface}));
+
+    std::map<std::string, std::vector<std::string>> mapperResponse;
+    try
+    {
+        auto mapperResponseMsg = bus.call(mapperCall);
+        mapperResponseMsg.read(mapperResponse);
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        lg2::error(
+            "Error in mapper call to get service name, path: {PATH}, interface: {INTERFACE}, error: {ERROR}",
+            "PATH", path, "INTERFACE", interface, "ERROR", e);
+        elog<InternalFailure>();
+    }
+
+    return mapperResponse.begin()->first;
+}
+
+GpioPresence::ObjectMap GpioPresence::getObjectMap(bool present)
+{
+    ObjectMap invObj;
+    InterfaceMap invIntf;
+    PropertyMap invProp;
+
+    invProp.emplace("Present", present);
+    invProp.emplace("PrettyName", name);
+    invIntf.emplace("xyz.openbmc_project.Inventory.Item", std::move(invProp));
+    // Add any extra interfaces we want to associate with the inventory item
+    for (auto& iface : interfaces)
+    {
+        invIntf.emplace(iface, PropertyMap());
+    }
+    invObj.emplace(std::move(inventory), std::move(invIntf));
+
+    return invObj;
+}
+
+void GpioPresence::updateInventory(bool present)
+{
+    ObjectMap invObj = getObjectMap(present);
+
+    lg2::info(
+        "Updating inventory present property value to {PRESENT}, path: {PATH}",
+        "PRESENT", present, "PATH", inventory);
+
+    auto bus = sdbusplus::bus::new_default();
+    auto invService = getService(INVENTORY_PATH, INVENTORY_INTF, bus);
+
+    // Update inventory
+    auto invMsg = bus.new_method_call(invService.c_str(), INVENTORY_PATH,
+                                      INVENTORY_INTF, "Notify");
+    invMsg.append(std::move(invObj));
+    try
+    {
+        auto invMgrResponseMsg = bus.call(invMsg);
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        lg2::error(
+            "Error in inventory manager call to update inventory: {ERROR}",
+            "ERROR", e);
+        elog<InternalFailure>();
+    }
+}
+
+void GpioPresence::scheduleEventHandler()
+{
+    std::string gpio = std::string(gpioLineMsg);
+
+    gpioEventDescriptor.async_wait(
+        boost::asio::posix::stream_descriptor::wait_read,
+        [this, gpio](const boost::system::error_code& ec) {
+        if (ec == boost::asio::error::operation_aborted)
+        {
+            // we were cancelled
+            return;
+        }
+        if (ec)
+        {
+            lg2::error("{GPIO} event handler error: {ERROR}", "GPIO", gpio,
+                       "ERROR", ec.message());
+            return;
+        }
+        gpioEventHandler();
+        });
+}
+
+void GpioPresence::cancelEventHandler()
+{
+    gpioEventDescriptor.cancel();
+}
+
+void GpioPresence::gpioEventHandler()
+{
+    gpiod_line_event gpioLineEvent;
+
+    if (gpiod_line_event_read_fd(gpioEventDescriptor.native_handle(),
+                                 &gpioLineEvent) < 0)
+    {
+        lg2::error("Failed to read {GPIO} from fd", "GPIO", gpioLineMsg);
+        return;
+    }
+
+    if (gpioLineEvent.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
+    {
+        lg2::info("{GPIO} Asserted", "GPIO", gpioLineMsg);
+    }
+    else
+    {
+        lg2::info("{GPIO} Deasserted", "GPIO", gpioLineMsg);
+    }
+    updateInventory(gpioLineEvent.event_type == GPIOD_LINE_EVENT_RISING_EDGE);
+
+    /* Schedule a wait event */
+    scheduleEventHandler();
+}
+
+int GpioPresence::requestGPIOEvents()
+{
+    std::string flags;
+
+    /* Request an event to monitor for respected gpio line */
+    if (gpiod_line_request(gpioLine, &gpioConfig, 0) < 0)
+    {
+        lg2::error("Failed to request {GPIO}: {ERRNO}", "GPIO", gpioLineMsg,
+                   "ERRNO", errno);
+        return -1;
+    }
+
+    int gpioLineFd = gpiod_line_event_get_fd(gpioLine);
+    if (gpioLineFd < 0)
+    {
+        lg2::error("Failed to get fd for {GPIO}", "GPIO", gpioLineMsg);
+        return -1;
+    }
+
+    if (gpioConfig.flags & GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLE)
+    {
+        flags += " Bias DISABLE";
+    }
+    else if (gpioConfig.flags & GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP)
+    {
+        flags += " Bias PULL_UP";
+    }
+    else if (gpioConfig.flags & GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN)
+    {
+        flags += " Bias PULL_DOWN";
+    }
+
+    if (gpioConfig.flags & GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW)
+    {
+        flags += " ActiveLow";
+    }
+
+    if (!flags.empty())
+    {
+        flags = "[" + flags + "]";
+    }
+
+    lg2::info("{GPIO} {FLAGS} monitoring started", "GPIO", gpioLineMsg, "FLAGS",
+              flags);
+
+    /* Assign line fd to descriptor for monitoring */
+    gpioEventDescriptor.assign(gpioLineFd);
+
+    updateInventory(gpiod_line_get_value(gpioLine));
+
+    /* Schedule a wait event */
+    scheduleEventHandler();
+
+    return 0;
+}
+} // namespace gpio
+} // namespace phosphor
diff --git a/multi-presence/gpio_presence.hpp b/multi-presence/gpio_presence.hpp
new file mode 100644
index 0000000..abc4b63
--- /dev/null
+++ b/multi-presence/gpio_presence.hpp
@@ -0,0 +1,133 @@
+#pragma once
+
+#include <gpiod.h>
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/posix/stream_descriptor.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <cstdlib>
+#include <filesystem>
+#include <map>
+#include <string>
+#include <vector>
+
+static constexpr auto deviceField = 0;
+static constexpr auto pathField = 1;
+using Device = std::string;
+using Path = std::filesystem::path;
+using Driver = std::tuple<Device, Path>;
+using Interface = std::string;
+
+namespace phosphor
+{
+namespace gpio
+{
+
+/** @class GpioPresence
+ *  @brief Responsible for catching GPIO state change
+ *  condition and updating the inventory presence.
+ */
+class GpioPresence
+{
+    using Property = std::string;
+    using Value = std::variant<bool, std::string>;
+    // Association between property and its value
+    using PropertyMap = std::map<Property, Value>;
+    using Interface = std::string;
+    // Association between interface and the D-Bus property
+    using InterfaceMap = std::map<Interface, PropertyMap>;
+    using Object = sdbusplus::message::object_path;
+    // Association between object and the interface
+    using ObjectMap = std::map<Object, InterfaceMap>;
+
+  public:
+    GpioPresence() = delete;
+    ~GpioPresence() = default;
+    GpioPresence(const GpioPresence&) = delete;
+    GpioPresence& operator=(const GpioPresence&) = delete;
+
+    /** @brief Constructs GpioPresence object.
+     *
+     *  @param[in] line             - GPIO line from libgpiod
+     *  @param[in] config           - configuration of line with event
+     *  @param[in] io               - io service
+     *  @param[in] inventory        - Object path under inventory that
+                                      will be created
+     *  @param[in] extraInterfaces  - List of interfaces to associate to
+                                      inventory item
+     *  @param[in] name             - PrettyName of inventory object
+     *  @param[in] lineMsg          - GPIO line message to be used for log
+     */
+    GpioPresence(gpiod_line* line, gpiod_line_request_config& config,
+                 boost::asio::io_context& io, const std::string& inventory,
+                 const std::vector<std::string>& extraInterfaces,
+                 const std::string& name, const std::string& lineMsg) :
+        gpioLine(line),
+        gpioConfig(config), gpioEventDescriptor(io), inventory(inventory),
+        interfaces(extraInterfaces), name(name), gpioLineMsg(lineMsg)
+    {
+        requestGPIOEvents();
+    };
+
+    GpioPresence(GpioPresence&& old) noexcept :
+        gpioLine(old.gpioLine), gpioConfig(old.gpioConfig),
+        gpioEventDescriptor(old.gpioEventDescriptor.get_executor()),
+        inventory(std::move(old.inventory)),
+        interfaces(std::move(old.interfaces)), name(std::move(old.name)),
+        gpioLineMsg(std::move(old.gpioLineMsg))
+    {
+        old.cancelEventHandler();
+
+        gpioEventDescriptor = std::move(old.gpioEventDescriptor);
+
+        scheduleEventHandler();
+    };
+
+  private:
+    /** @brief GPIO line */
+    gpiod_line* gpioLine;
+
+    /** @brief GPIO line configuration */
+    gpiod_line_request_config gpioConfig;
+
+    /** @brief GPIO event descriptor */
+    boost::asio::posix::stream_descriptor gpioEventDescriptor;
+
+    /** @brief Object path under inventory that will be created */
+    const std::string inventory;
+
+    /** @brief List of interfaces to associate to inventory item */
+    const std::vector<std::string> interfaces;
+
+    /** @brief PrettyName of inventory object */
+    const std::string name;
+
+    /** @brief GPIO line name message */
+    const std::string gpioLineMsg;
+
+    /** @brief register handler for gpio event
+     *
+     *  @return  - 0 on success and -1 otherwise
+     */
+    int requestGPIOEvents();
+
+    /** @brief Schedule an event handler for GPIO event to trigger */
+    void scheduleEventHandler();
+
+    /** @brief Stop the event handler for GPIO events */
+    void cancelEventHandler();
+
+    /** @brief Handle the GPIO event and starts configured target */
+    void gpioEventHandler();
+
+    /** @brief Returns the object map for the inventory object */
+    ObjectMap getObjectMap(bool present);
+
+    /** @brief Updates the inventory */
+    void updateInventory(bool present);
+};
+
+} // namespace gpio
+} // namespace phosphor
diff --git a/multi-presence/main.cpp b/multi-presence/main.cpp
new file mode 100644
index 0000000..4b631d9
--- /dev/null
+++ b/multi-presence/main.cpp
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2019 Facebook
+ * Copyright © 2023 9elements GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "gpio_presence.hpp"
+
+#include <CLI/CLI.hpp>
+#include <boost/asio/io_context.hpp>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <fstream>
+
+namespace phosphor
+{
+namespace gpio
+{
+
+const std::map<std::string, int> biasMap = {
+    /**< Set bias as is. */
+    {"AS_IS", 0},
+    /**< Disable bias. */
+    {"DISABLE", GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLE},
+    /**< Enable pull-up. */
+    {"PULL_UP", GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP},
+    /**< Enable pull-down. */
+    {"PULL_DOWN", GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN}};
+}
+} // namespace phosphor
+
+int main(int argc, char** argv)
+{
+    boost::asio::io_context io;
+
+    CLI::App app{"Monitor gpio presence status"};
+
+    std::string gpioFileName;
+
+    /* Add an input option */
+    app.add_option("-c,--config", gpioFileName, "Name of config json file")
+        ->required()
+        ->check(CLI::ExistingFile);
+
+    /* Parse input parameter */
+    try
+    {
+        app.parse(argc, argv);
+    }
+    catch (const CLI::Error& e)
+    {
+        return app.exit(e);
+    }
+
+    /* Get list of gpio config details from json file */
+    std::ifstream file(gpioFileName);
+    if (!file)
+    {
+        lg2::error("Failed to open config file: {FILE}", "FILE", gpioFileName);
+        return -1;
+    }
+
+    nlohmann::json gpioMonObj;
+    file >> gpioMonObj;
+    file.close();
+
+    std::vector<phosphor::gpio::GpioPresence> gpios;
+
+    for (auto& obj : gpioMonObj)
+    {
+        /* GPIO Line message */
+        std::string lineMsg = "GPIO Line ";
+
+        /* GPIO line */
+        gpiod_line* line = NULL;
+
+        /* GPIO line configuration, default to monitor both edge */
+        struct gpiod_line_request_config config
+        {
+            "gpio_monitor", GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES, 0
+        };
+
+        /* Pretty name of the inventory object */
+        std::string name;
+
+        /* Object path under inventory that will be created */
+        std::string inventory;
+
+        /* List of interfaces to associate to inventory item */
+        std::vector<std::string> extraInterfaces;
+
+        if (obj.find("LineName") == obj.end())
+        {
+            /* If there is no line Name defined then gpio num nd chip
+             * id must be defined. GpioNum is integer mapping to the
+             * GPIO key configured by the kernel
+             */
+            if (obj.find("GpioNum") == obj.end() ||
+                obj.find("ChipId") == obj.end())
+            {
+                lg2::error("Failed to find line name or gpio number: {FILE}",
+                           "FILE", gpioFileName);
+                return -1;
+            }
+
+            std::string chipIdStr = obj["ChipId"].get<std::string>();
+            int gpioNum = obj["GpioNum"].get<int>();
+
+            lineMsg += chipIdStr + " " + std::to_string(gpioNum);
+
+            /* Get the GPIO line */
+            line = gpiod_line_get(chipIdStr.c_str(), gpioNum);
+        }
+        else
+        {
+            /* Find the GPIO line */
+            std::string lineName = obj["LineName"].get<std::string>();
+            lineMsg += lineName;
+            line = gpiod_line_find(lineName.c_str());
+        }
+
+        if (line == NULL)
+        {
+            lg2::error("Failed to find the {GPIO}", "GPIO", lineMsg);
+            return -1;
+        }
+
+        /* Parse out inventory argument. */
+        if (obj.find("Inventory") == obj.end())
+        {
+            lg2::error("{GPIO}: Inventory path not specified", "GPIO", lineMsg);
+            return -1;
+        }
+        else
+        {
+            inventory = obj["Inventory"].get<std::string>();
+        }
+
+        if (obj.find("Name") == obj.end())
+        {
+            lg2::error("{GPIO}: Name path not specified", "GPIO", lineMsg);
+            return -1;
+        }
+        else
+        {
+            name = obj["Name"].get<std::string>();
+        }
+
+        /* Parse optional bias */
+        if (obj.find("Bias") != obj.end())
+        {
+            std::string biasName = obj["Bias"].get<std::string>();
+            auto findBias = phosphor::gpio::biasMap.find(biasName);
+            if (findBias == phosphor::gpio::biasMap.end())
+            {
+                lg2::error("{GPIO}: Bias unknown: {BIAS}", "GPIO", lineMsg,
+                           "BIAS", biasName);
+                return -1;
+            }
+
+            config.flags = findBias->second;
+        }
+
+        /* Parse optional active level */
+        if (obj.find("ActiveLow") != obj.end() && obj["ActiveLow"].get<bool>())
+        {
+            config.flags |= GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
+        }
+
+        /* Parse optional extra interfaces */
+        if (obj.find("ExtraInterfaces") != obj.end())
+        {
+            obj.at("ExtraInterfaces").get_to(extraInterfaces);
+        }
+
+        /* Create a monitor object and let it do all the rest */
+        gpios.push_back(phosphor::gpio::GpioPresence(
+            line, config, io, inventory, extraInterfaces, name, lineMsg));
+    }
+    io.run();
+
+    return 0;
+}
diff --git a/multi-presence/meson.build b/multi-presence/meson.build
new file mode 100644
index 0000000..be1f97c
--- /dev/null
+++ b/multi-presence/meson.build
@@ -0,0 +1,16 @@
+executable(
+    'phosphor-multi-gpio-presence',
+    'gpio_presence.cpp',
+    'main.cpp',
+    dependencies: [
+        cli11_dep,
+        libgpiod,
+        nlohmann_json_dep,
+        phosphor_logging,
+        sdbusplus,
+    ],
+    cpp_args: boost_args,
+    include_directories: '..',
+    implicit_include_directories: false,
+    install: true,
+)
\ No newline at end of file
diff --git a/phosphor-multi-gpio-presence.json b/phosphor-multi-gpio-presence.json
new file mode 100644
index 0000000..65b5283
--- /dev/null
+++ b/phosphor-multi-gpio-presence.json
@@ -0,0 +1,16 @@
+[
+    {
+        "Name": "DIMM A0",
+        "LineName": "POWER_BUTTON",
+        "Inventory": "/system/chassis/motherboard/dimm_a0"
+    },
+    {
+        "Name": "Powersupply 0",
+        "ChipId": "0",
+        "GpioNum": 14,
+        "Inventory": "/system/chassis/motherboard/powersupply0",
+        "ActiveLow": true,
+        "Bias": "PULL_UP",
+        "ExtraInterfaces": ["xyz.openbmc_project.Inventory.Item.PowerSupply"]
+    }
+]
diff --git a/phosphor-multi-gpio-presence.service b/phosphor-multi-gpio-presence.service
new file mode 100644
index 0000000..f3c9c1a
--- /dev/null
+++ b/phosphor-multi-gpio-presence.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Phosphor Multi GPIO presence
+Wants=mapper-wait@-xyz-openbmc_project-inventory.service
+After=mapper-wait@-xyz-openbmc_project-inventory.service
+
+[Service]
+Restart=no
+ExecStart=/usr/bin/phosphor-multi-gpio-presence --config /usr/share/phosphor-gpio-monitor/phosphor-multi-gpio-presence.json
+
+[Install]
+RequiredBy=multi-user.target
\ No newline at end of file