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/docs/config_file/power_sequencer.md b/phosphor-power-sequencer/docs/config_file/power_sequencer.md
index d57cac7..f387303 100644
--- a/phosphor-power-sequencer/docs/config_file/power_sequencer.md
+++ b/phosphor-power-sequencer/docs/config_file/power_sequencer.md
@@ -22,14 +22,20 @@
 
 ## Properties
 
-| Name                    | Required | Type                              | Description                                                                                                                   |
-| :---------------------- | :------: | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
-| comments                |    no    | array of strings                  | One or more comment lines describing this power sequencer.                                                                    |
-| type                    |   yes    | string                            | Power sequencer type. Specify one of the following supported types: "UCD90160", "UCD90320".                                   |
-| i2c_interface           |   yes    | [i2c_interface](i2c_interface.md) | I2C interface to this power sequencer.                                                                                        |
-| power_control_gpio_name |   yes    | string                            | Named GPIO for turning this power sequencer on and off.                                                                       |
-| power_good_gpio_name    |   yes    | string                            | Named GPIO for reading the power good signal from this power sequencer.                                                       |
-| rails                   |   yes    | array of [rails](rail.md)         | One or more voltage rails powered on/off and monitored by this power sequencer. The rails must be in power on sequence order. |
+| Name                    |      Required       | Type                              | Description                                                                                                                   |
+| :---------------------- | :-----------------: | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
+| comments                |         no          | array of strings                  | One or more comment lines describing this power sequencer.                                                                    |
+| type                    |         yes         | string                            | Power sequencer type. Specify one of the following supported types: "UCD90160", "UCD90320", "gpios_only_device".              |
+| i2c_interface           | see [notes](#notes) | [i2c_interface](i2c_interface.md) | I2C interface to this power sequencer.                                                                                        |
+| power_control_gpio_name |         yes         | string                            | Named GPIO for turning this power sequencer on and off.                                                                       |
+| power_good_gpio_name    |         yes         | string                            | Named GPIO for reading the power good signal from this power sequencer.                                                       |
+| rails                   | see [notes](#notes) | array of [rails](rail.md)         | One or more voltage rails powered on/off and monitored by this power sequencer. The rails must be in power on sequence order. |
+
+### Notes
+
+- The "i2c_interface" and "rails" properties are required if the "type" is
+  "UCD90160" or "UCD90320". They are ignored if the "type" is
+  "gpios_only_device".
 
 ## Example
 
diff --git a/phosphor-power-sequencer/docs/internal_design.md b/phosphor-power-sequencer/docs/internal_design.md
index 82b7ecd..5aab2f4 100644
--- a/phosphor-power-sequencer/docs/internal_design.md
+++ b/phosphor-power-sequencer/docs/internal_design.md
@@ -29,6 +29,9 @@
 - PowerSequencerDevice
   - Abstract base class for a power sequencer device.
   - Defines virtual methods that must be implemented by all child classes.
+- GPIOsOnlyDevice
+  - Sub-class of PowerSequencerDevice that only uses the named GPIOs and does
+    not otherwise communicate with the device.
 - StandardDevice
   - Sub-class of PowerSequencerDevice that implements the standard pgood fault
     detection algorithm.
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
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index 78899bb..08b6218 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -1655,6 +1655,27 @@
         EXPECT_EQ(powerSequencer->getRails()[1]->getName(), "cpu2");
     }
 
+    // Test where works: Type is "gpios_only_device"
+    {
+        const json element = R"(
+            {
+              "type": "gpios_only_device",
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto powerSequencer = parsePowerSequencer(element, variables, services);
+        EXPECT_EQ(powerSequencer->getName(), "gpios_only_device");
+        EXPECT_EQ(powerSequencer->getBus(), 0);
+        EXPECT_EQ(powerSequencer->getAddress(), 0);
+        EXPECT_EQ(powerSequencer->getPowerControlGPIOName(),
+                  "power-chassis-control");
+        EXPECT_EQ(powerSequencer->getPowerGoodGPIOName(), "power-chassis-good");
+        EXPECT_EQ(powerSequencer->getRails().size(), 0);
+    }
+
     // Test where fails: Element is not an object
     try
     {
diff --git a/phosphor-power-sequencer/test/gpios_only_device_tests.cpp b/phosphor-power-sequencer/test/gpios_only_device_tests.cpp
new file mode 100644
index 0000000..8bb2df1
--- /dev/null
+++ b/phosphor-power-sequencer/test/gpios_only_device_tests.cpp
@@ -0,0 +1,196 @@
+/**
+ * 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.
+ */
+
+#include "gpios_only_device.hpp"
+#include "mock_services.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+
+TEST(GPIOsOnlyDeviceTests, Constructor)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerControlGPIOName(), powerControlGPIOName);
+    EXPECT_EQ(device.getPowerGoodGPIOName(), powerGoodGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetName)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getName(), GPIOsOnlyDevice::deviceName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetBus)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getBus(), 0);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetAddress)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getAddress(), 0);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetPowerControlGPIOName)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerControlGPIOName(), powerControlGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetPowerGoodGPIOName)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerGoodGPIOName(), powerGoodGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetRails)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_TRUE(device.getRails().empty());
+}
+
+TEST(GPIOsOnlyDeviceTests, GetGPIOValues)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        MockServices services{};
+        device.getGPIOValues(services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getGPIOValues() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetStatusWord)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getStatusWord(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getStatusWord() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetStatusVout)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getStatusVout(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getStatusVout() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetReadVout)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getReadVout(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getReadVout() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetVoutUVFaultLimit)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getVoutUVFaultLimit(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getVoutUVFaultLimit() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, FindPgoodFault)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    MockServices services{};
+    std::string powerSupplyError{};
+    std::map<std::string, std::string> additionalData{};
+    std::string error =
+        device.findPgoodFault(services, powerSupplyError, additionalData);
+    EXPECT_TRUE(error.empty());
+    EXPECT_EQ(additionalData.size(), 0);
+}
diff --git a/phosphor-power-sequencer/test/meson.build b/phosphor-power-sequencer/test/meson.build
index 2827c3f..5de4510 100644
--- a/phosphor-power-sequencer/test/meson.build
+++ b/phosphor-power-sequencer/test/meson.build
@@ -4,6 +4,7 @@
         'phosphor-power-sequencer-tests',
         'chassis_tests.cpp',
         'config_file_parser_tests.cpp',
+        'gpios_only_device_tests.cpp',
         'pmbus_driver_device_tests.cpp',
         'rail_tests.cpp',
         'standard_device_tests.cpp',