pseq: Support "GPIOs only" power sequencer device

Add support for a "GPIOs only" power sequencer device to the
phosphor-power-sequencer application.

If this device type is specified in the JSON configuration file, then
the application will only use the named GPIOs to power the device on/off
and read the power good signal. No attempt will be made to communicate
with the device otherwise. No pgood fault isolation will be performed.

This device type is useful for simple systems that do not require pgood
fault isolation. It is also useful as a temporary solution when
performing early bring-up work on a new system.

Tested:
* Ran automated tests

Change-Id: Ib5ba9a9c136dd5f5e853372f881f9e227f01a6bd
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/config_file_parser.cpp b/phosphor-power-sequencer/src/config_file_parser.cpp
index 2a5cc26..07a92c3 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -17,6 +17,7 @@
 #include "config_file_parser.hpp"
 
 #include "config_file_parser_error.hpp"
+#include "gpios_only_device.hpp"
 #include "json_parser_utils.hpp"
 #include "ucd90160_device.hpp"
 #include "ucd90320_device.hpp"
@@ -333,11 +334,19 @@
     std::string type = parseString(typeElement, false, variables);
     ++propertyCount;
 
-    // Required i2c_interface property
-    const json& i2cInterfaceElement =
-        getRequiredProperty(element, "i2c_interface");
-    auto [bus, address] = parseI2CInterface(i2cInterfaceElement, variables);
-    ++propertyCount;
+    // i2c_interface property is required for some device types
+    uint8_t bus{0};
+    uint16_t address{0};
+    auto i2cInterfaceIt = element.find("i2c_interface");
+    if (i2cInterfaceIt != element.end())
+    {
+        std::tie(bus, address) = parseI2CInterface(*i2cInterfaceIt, variables);
+        ++propertyCount;
+    }
+    else if (type != GPIOsOnlyDevice::deviceName)
+    {
+        throw std::invalid_argument{"Required property missing: i2c_interface"};
+    }
 
     // Required power_control_gpio_name property
     const json& powerControlGPIONameElement =
@@ -353,11 +362,18 @@
         parseString(powerGoodGPIONameElement, false, variables);
     ++propertyCount;
 
-    // Required rails property
-    const json& railsElement = getRequiredProperty(element, "rails");
-    std::vector<std::unique_ptr<Rail>> rails =
-        parseRailArray(railsElement, variables);
-    ++propertyCount;
+    // rails property is required for some device types
+    std::vector<std::unique_ptr<Rail>> rails{};
+    auto railsIt = element.find("rails");
+    if (railsIt != element.end())
+    {
+        rails = parseRailArray(*railsIt, variables);
+        ++propertyCount;
+    }
+    else if (type != GPIOsOnlyDevice::deviceName)
+    {
+        throw std::invalid_argument{"Required property missing: rails"};
+    }
 
     // Verify no invalid properties exist
     verifyPropertyCount(element, propertyCount);
@@ -374,6 +390,11 @@
             bus, address, powerControlGPIOName, powerGoodGPIOName,
             std::move(rails), services);
     }
+    else if (type == GPIOsOnlyDevice::deviceName)
+    {
+        return std::make_unique<GPIOsOnlyDevice>(powerControlGPIOName,
+                                                 powerGoodGPIOName);
+    }
     throw std::invalid_argument{"Invalid power sequencer type: " + type};
 }
 
diff --git a/phosphor-power-sequencer/src/gpios_only_device.hpp b/phosphor-power-sequencer/src/gpios_only_device.hpp
new file mode 100644
index 0000000..36dc6f2
--- /dev/null
+++ b/phosphor-power-sequencer/src/gpios_only_device.hpp
@@ -0,0 +1,168 @@
+/**
+ * Copyright © 2025 IBM Corporation
+ *
+ * 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.
+ */
+#pragma once
+
+#include "power_sequencer_device.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class GPIOsOnlyDevice
+ *
+ * PowerSequencerDevice sub-class that only uses the named GPIOs.
+ *
+ * This class uses named GPIOs to power the device on/off and read the power
+ * good signal from the device.
+ *
+ * No other communication is performed to the device over I2C or through a
+ * device driver. If a pgood fault occurs, no attempt will be made to determine
+ * which voltage rail caused the fault.
+ *
+ * This device type is useful for simple systems that do not require pgood fault
+ * isolation. It is also useful as a temporary solution when performing early
+ * bring-up work on a new system.
+ */
+class GPIOsOnlyDevice : public PowerSequencerDevice
+{
+  public:
+    GPIOsOnlyDevice() = delete;
+    GPIOsOnlyDevice(const GPIOsOnlyDevice&) = delete;
+    GPIOsOnlyDevice(GPIOsOnlyDevice&&) = delete;
+    GPIOsOnlyDevice& operator=(const GPIOsOnlyDevice&) = delete;
+    GPIOsOnlyDevice& operator=(GPIOsOnlyDevice&&) = delete;
+    virtual ~GPIOsOnlyDevice() = default;
+
+    /**
+     * Constructor.
+     *
+     * @param powerControlGPIOName name of the GPIO that turns this device on
+     *                             and off
+     * @param powerGoodGPIOName name of the GPIO that reads the power good
+     *                          signal from this device
+     */
+    explicit GPIOsOnlyDevice(const std::string& powerControlGPIOName,
+                             const std::string& powerGoodGPIOName) :
+        powerControlGPIOName{powerControlGPIOName},
+        powerGoodGPIOName{powerGoodGPIOName}
+    {}
+
+    /** @copydoc PowerSequencerDevice::getName() */
+    virtual const std::string& getName() const override
+    {
+        return deviceName;
+    }
+
+    /** @copydoc PowerSequencerDevice::getBus() */
+    virtual uint8_t getBus() const override
+    {
+        return 0;
+    }
+
+    /** @copydoc PowerSequencerDevice::getAddress() */
+    virtual uint16_t getAddress() const override
+    {
+        return 0;
+    }
+
+    /** @copydoc PowerSequencerDevice::getPowerControlGPIOName() */
+    virtual const std::string& getPowerControlGPIOName() const override
+    {
+        return powerControlGPIOName;
+    }
+
+    /** @copydoc PowerSequencerDevice::getPowerGoodGPIOName() */
+    virtual const std::string& getPowerGoodGPIOName() const override
+    {
+        return powerGoodGPIOName;
+    }
+
+    /** @copydoc PowerSequencerDevice::getRails() */
+    virtual const std::vector<std::unique_ptr<Rail>>& getRails() const override
+    {
+        return rails;
+    }
+
+    /** @copydoc PowerSequencerDevice::getGPIOValues() */
+    virtual std::vector<int> getGPIOValues(
+        [[maybe_unused]] Services& services) override
+    {
+        throw std::logic_error{"getGPIOValues() is not supported"};
+    }
+
+    /** @copydoc PowerSequencerDevice::getStatusWord() */
+    virtual uint16_t getStatusWord([[maybe_unused]] uint8_t page) override
+    {
+        throw std::logic_error{"getStatusWord() is not supported"};
+    }
+
+    /** @copydoc PowerSequencerDevice::getStatusVout() */
+    virtual uint8_t getStatusVout([[maybe_unused]] uint8_t page) override
+    {
+        throw std::logic_error{"getStatusVout() is not supported"};
+    }
+
+    /** @copydoc PowerSequencerDevice::getReadVout() */
+    virtual double getReadVout([[maybe_unused]] uint8_t page) override
+    {
+        throw std::logic_error{"getReadVout() is not supported"};
+    }
+
+    /** @copydoc PowerSequencerDevice::getVoutUVFaultLimit() */
+    virtual double getVoutUVFaultLimit([[maybe_unused]] uint8_t page) override
+    {
+        throw std::logic_error{"getVoutUVFaultLimit() is not supported"};
+    }
+
+    /** @copydoc PowerSequencerDevice::findPgoodFault() */
+    virtual std::string findPgoodFault(
+        [[maybe_unused]] Services& services,
+        [[maybe_unused]] const std::string& powerSupplyError,
+        [[maybe_unused]] std::map<std::string, std::string>& additionalData)
+        override
+    {
+        return std::string{};
+    }
+
+    inline static const std::string deviceName{"gpios_only_device"};
+
+  protected:
+    /**
+     * Name of the GPIO that turns this device on and off.
+     */
+    std::string powerControlGPIOName{};
+
+    /**
+     * Name of the GPIO that reads the power good signal from this device.
+     */
+    std::string powerGoodGPIOName{};
+
+    /**
+     * Empty list of voltage rails to return from getRails().
+     */
+    std::vector<std::unique_ptr<Rail>> rails{};
+};
+
+} // namespace phosphor::power::sequencer