Add a platform init daemon

This commit adds a daemon that can initialize platforms, and adds in the
requisite CLI interfaces to be able to add new platforms in the future.

Change-Id: If4a801fe38e9f4141737d2b5c8b494dc00bf4d59
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..02cf3c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/subprojects/*
+!/subprojects/*.wrap
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..26dbac2
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,33 @@
+project(
+    'platform-init',
+    'cpp',
+    meson_version: '>=1.7.0',
+    version: '0.1',
+    default_options: ['warning_level=3', 'cpp_std=c++23'],
+)
+
+gpiodcxx_dep = dependency('libgpiodcxx', default_options: ['bindings=cxx'])
+systemd_dep = dependency('systemd')
+libsystemd_dep = dependency('libsystemd')
+
+cli11_dep = dependency('CLI11', required: true, include_type: 'system')
+
+exe = executable(
+    'platform',
+    'platform.cpp',
+    dependencies: [gpiodcxx_dep, libsystemd_dep, cli11_dep],
+    include_directories: ['.'],
+    install: true,
+    install_dir: get_option('libexecdir'),
+)
+
+systemd_system_unit_dir = systemd_dep.get_variable('systemd_system_unit_dir')
+configure_file(
+    input: 'platform_init.service.in',
+    output: 'platform_init.service',
+    install_dir: systemd_system_unit_dir,
+    install: true,
+    configuration: configuration_data(
+        {'PLATFORM_NAME': get_option('platform-name')},
+    ),
+)
diff --git a/meson.options b/meson.options
new file mode 100644
index 0000000..9004923
--- /dev/null
+++ b/meson.options
@@ -0,0 +1,6 @@
+option(
+    'platform-name',
+    type: 'string',
+    value: 'nvidia-gb200',
+    description: 'Platform to init (Temporary)',
+)
diff --git a/platform.cpp b/platform.cpp
new file mode 100644
index 0000000..7a947cf
--- /dev/null
+++ b/platform.cpp
@@ -0,0 +1,464 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2025 NVIDIA
+
+#include <fcntl.h>
+#include <systemd/sd-daemon.h>
+
+#include <CLI/CLI.hpp>
+#include <gpiod.hpp>
+
+#include <chrono>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <format>
+#include <fstream>
+#include <iostream>
+#include <thread>
+#include <unordered_map>
+
+using namespace std::chrono_literals;
+
+constexpr static const char* app_name = "platform_init";
+
+// Map of GPIO name to line.  Holds lines open for the duration of the program
+static std::unordered_map<std::string, gpiod::line> io;
+
+void sleep_milliseconds(std::chrono::milliseconds milliseconds)
+{
+    std::cerr << std::format("Sleeping for {} milliseconds\n",
+                             milliseconds.count());
+    std::this_thread::sleep_for(milliseconds);
+}
+
+void set_gpio(const char* line_name, int value,
+              std::chrono::milliseconds find_timeout = 0ms)
+{
+    std::cerr << std::format("{} Request to set to {}\n", line_name, value);
+    std::chrono::milliseconds polling_time = 10ms;
+    gpiod::line& line = io[line_name];
+    if (!line)
+    {
+        do
+        {
+            line = gpiod::find_line(line_name);
+            if (!line)
+            {
+                std::cerr << std::format(
+                    "{} not found yet, waiting and retrying\n", line_name);
+
+                sleep_milliseconds(polling_time);
+                find_timeout -= polling_time;
+            }
+        } while (!line && find_timeout > 0s);
+        if (!line && find_timeout <= 0s)
+        {
+            std::cerr << std::format("{} Unable to find\n", line_name);
+            return;
+        }
+        try
+        {
+            line.request({app_name, gpiod::line_request::DIRECTION_OUTPUT, 0},
+                         value);
+        }
+        catch (const std::system_error& e)
+        {
+            std::cerr << std::format(
+                "{} unable to set direction and value {}\n", line_name,
+                e.what());
+            return;
+        }
+        // No need to set if the init did it for us
+        std::cerr << std::format("{} Set to {}\n", line_name, value);
+        return;
+    }
+    std::cerr << std::format("{} Settingto {}\n", line_name, value);
+    line.set_value(value);
+}
+
+int get_gpio(const char* line_name)
+{
+    std::cerr << std::format("{} Request to get\n", line_name);
+
+    gpiod::line line = gpiod::find_line(line_name);
+    if (!line)
+    {
+        std::cerr << std::format("{} Set unable to find\n", line_name);
+        return -1;
+    }
+    try
+    {
+        line.request({app_name, gpiod::line_request::DIRECTION_INPUT, 0});
+    }
+    catch (const std::system_error& e)
+    {
+        std::cerr << std::format("{} unable to set {}\n", line_name, e.what());
+    }
+
+    int value = line.get_value();
+    std::cerr << std::format("{} was {}\n", line_name, value);
+    return value;
+}
+
+enum class GpioEventResult
+{
+    Error,
+    Asserted,
+    Timeout
+};
+
+struct GpioEvent
+{
+    GpioEvent(const char* line_name_in, int value_in) :
+        line_name(line_name_in), value(value_in)
+    {
+        line = gpiod::find_line(line_name);
+        if (!line)
+        {
+            std::cerr << std::format("{} GpioEvent: Unable to find\n",
+                                     line_name);
+            return;
+        }
+        int edge = (value != 0) ? ::gpiod::line_request::EVENT_RISING_EDGE
+                                : ::gpiod::line_request::EVENT_FALLING_EDGE;
+
+        line.request({app_name, edge, 0});
+
+        int val = line.get_value();
+        if (val == value)
+        {
+            std::cerr << std::format("{} GpioEvent is already {}\n", line_name,
+                                     val);
+        }
+        else
+        {
+            std::cerr << std::format("GpioEvent created for {}\n", line_name);
+        }
+    }
+    GpioEventResult wait()
+    {
+        if (!line)
+        {
+            std::cerr << std::format("Line {} wasn't initialized\n", line_name);
+            return GpioEventResult::Error;
+        }
+        std::cerr << std::format("{}  Waiting to go to {}\n", line_name,
+                                 (value != 0) ? "assert" : "deassert");
+        auto events = line.event_wait(std::chrono::seconds(120));
+        if (!events)
+        {
+            std::cerr << std::format("{} Timeout\n", line_name);
+            return GpioEventResult::Timeout;
+        }
+
+        std::cerr << std::format("{} Asserted\n", line_name);
+
+        return GpioEventResult::Asserted;
+    }
+
+    gpiod::line line;
+    std::string line_name;
+    int value;
+};
+
+void rebind_i2c(std::string number)
+{
+    std::string bindpath =
+        std::format("/sys/bus/platform/drivers/aspeed-i2c-bus/unbind", number);
+    std::ofstream bindofs(bindpath);
+    if (!bindofs)
+    {
+        std::cerr << std::format("{} unable to open\n", bindpath);
+        return;
+    }
+    try
+    {
+        bindofs << std::format("{}.i2c\n", number);
+    }
+    catch (const std::system_error& e)
+    {
+        std::cerr << std::format("{} unable to write\n", bindpath);
+        return;
+    }
+    bindofs.close();
+    std::cerr << std::format("{} unbound\n", number);
+
+    std::string unbindpath =
+        std::format("/sys/bus/platform/drivers/aspeed-i2c-bus/bind", number);
+    std::ofstream unbindofs(unbindpath);
+    if (!unbindofs)
+    {
+        std::cerr << std::format("{} unable to open\n", unbindpath);
+        return;
+    }
+    try
+    {
+        unbindofs << std::format("{}.i2c\n", number);
+    }
+    catch (const std::system_error& e)
+    {
+        std::cerr << std::format("{} unable to write\n", unbindpath);
+        return;
+    }
+    std::cerr << std::format("{} bound\n", number);
+}
+
+void set_gpio_raw(unsigned int chip_num, unsigned int bit_num, int value)
+{
+    std::string syspath = std::format("gpiochip{}", chip_num);
+    std::cerr << std::format("Setting gpiochip{} bit {} to {}\n", chip_num,
+                             bit_num, value);
+    try
+    {
+        gpiod::chip chip(syspath);
+        gpiod::line line = chip.get_line(bit_num);
+        line.request({app_name, gpiod::line_request::DIRECTION_OUTPUT, 0},
+                     value);
+        std::cerr << std::format("gpiochip{} bit {} set to {}\n", chip_num,
+                                 bit_num, value);
+    }
+    catch (const std::system_error& e)
+    {
+        std::cerr << std::format("Error setting gpiochip{} bit {}: {}\n",
+                                 chip_num, bit_num, e.what());
+    }
+}
+
+void new_device(unsigned int bus, unsigned int address,
+                std::string_view device_type)
+{
+    std::string path =
+        std::format("/sys/bus/i2c/devices/i2c-{}/new_device", bus);
+    std::cerr << std::format("attempting to open {}", path);
+    std::ofstream new_device(path);
+    if (!new_device)
+    {
+        std::cerr << "Error: Unable to create I2C device\n";
+        return;
+    }
+    new_device << std::format("{} 0x{:02x}", device_type, address);
+    new_device.close();
+
+    std::cerr << std::format("{} device created at bus {}", device_type, bus);
+}
+
+void wait_for_path_to_exist(std::string_view path,
+                            std::chrono::milliseconds timeout)
+{
+    while (true)
+    {
+        std::error_code ec;
+        bool exists = std::filesystem::exists(path, ec);
+        if (exists)
+        {
+            return;
+        }
+        sleep_milliseconds(1ms);
+        timeout -= 1ms;
+    }
+    std::cerr << std::format("Failed to wait for {} to exist", path);
+}
+
+void init_p2020_gpu_card()
+{
+    std::cerr << "Initializing GPU card...\n";
+
+    // Init the P2020 gpio expander
+    new_device(14, 0x20, "pca6408");
+
+    // Wait for device to be created
+    const auto* device_path = "/sys/bus/i2c/devices/14-0020";
+    wait_for_path_to_exist(device_path, 1000ms);
+
+    // Find the GPIO chip number
+    std::string gpio_chip;
+    for (const auto& entry : std::filesystem::directory_iterator(device_path))
+    {
+        std::string path = entry.path().string();
+        if (path.find("gpiochip") != std::string::npos)
+        {
+            gpio_chip =
+                path.substr(path.find("gpiochip") + std::strlen("gpiochip"));
+            break;
+        }
+    }
+    if (gpio_chip.empty())
+    {
+        std::cerr << "Error: Could not find GPIO chip number\n";
+        return;
+    }
+
+    std::cerr << "Found GPIO chip: gpiochip" << gpio_chip << "\n";
+    unsigned int gpiochipint = 0;
+    std::from_chars_result r =
+        std::from_chars(&*gpio_chip.begin(), &*gpio_chip.end(), gpiochipint);
+    if (r.ec != std::error_code() || r.ptr != &*gpio_chip.end())
+    {
+        std::cout << "Failed to convert gpiochip\n";
+        return;
+    }
+
+    // Set MCU in recovery
+    set_gpio_raw(gpiochipint, 3, 1);
+
+    // Reset MCU
+    set_gpio_raw(gpiochipint, 4, 0);
+    set_gpio_raw(gpiochipint, 4, 1);
+
+    // Switch MUX to MCU
+    set_gpio_raw(gpiochipint, 5, 1);
+}
+
+bool hmc_is_present()
+{
+    std::error_code ec;
+    bool exists = std::filesystem::exists("/sys/bus/i2c/devices/9-0074", ec);
+    if (ec)
+    {
+        exists = false;
+    }
+    if (exists)
+    {
+        std::cerr << "HMC present in platform";
+    }
+    else
+    {
+        std::cerr << "HMC not present in platform";
+    }
+    return exists;
+}
+
+int init_nvidia_gb200(bool has_p2020)
+{
+    // Reset USB hubs
+    set_gpio("USB_HUB_RESET_L-O", 0, 10000ms);
+    bool hmc_present = hmc_is_present();
+    if (!hmc_present)
+    {
+        set_gpio("SEC_USB2_HUB_RST_L-O", 0, 10000ms);
+    }
+
+    sleep_milliseconds(100ms);
+    if (!hmc_present)
+    {
+        set_gpio("SEC_USB2_HUB_RST_L-O", 1);
+    }
+    //  Write SGPIO_BMC_EN-O=1 to correctly set mux to send SGPIO signals to
+    //  FPGA
+    set_gpio("SGPIO_BMC_EN-O", 1);
+
+    // Write the bit for BMC without HMC
+    set_gpio("HMC_BMC_DETECT-O", static_cast<int>(!hmc_present), 30000ms);
+
+    // Set BMC_EROT_FPGA_SPI_MUX_SEL-O = 1 to enable FPGA to access its EROT
+    set_gpio("BMC_EROT_FPGA_SPI_MUX_SEL-O", 1);
+
+    // Enable 12V
+    set_gpio("BMC_12V_CTRL-O", 1, 10000ms);
+
+    set_gpio("PWR_BRAKE_L-O", 1);
+    set_gpio("SHDN_REQ_L-O", 1);
+    set_gpio("SHDN_FORCE_L-O", 1);
+    // Hold in reset (asserted) after standby power enabled
+    set_gpio("SYS_RST_IN_L-O", 0);
+
+    GpioEvent fpga_ready_wait = GpioEvent("FPGA_READY_BMC-I", 1);
+    GpioEvent sec_erot_fpga_rst = GpioEvent("SEC_FPGA_READY_BMC-I", 1);
+
+    // Release FPGA EROT from reset
+    set_gpio("EROT_FPGA_RST_L-O", 1);
+    set_gpio("SEC_EROT_FPGA_RST_L-O", 1);
+
+    sleep_milliseconds(100ms);
+
+    set_gpio("FPGA_RST_L-O", 1);
+
+    if (fpga_ready_wait.wait() != GpioEventResult::Asserted)
+    {
+        std::cerr << "FPGA_READY_BMC-I failed to assert\n";
+        // return EXIT_FAILURE;
+    }
+
+    if (sec_erot_fpga_rst.wait() != GpioEventResult::Asserted)
+    {
+        std::cerr << "SEC_FPGA_READY_BMC-I failed to assert\n";
+        // return EXIT_FAILURE;
+    }
+
+    // ReInitialize the FPGA connected I2C buses to unstick them and let
+    // FruDevice know it can scan for FRUs I2c bus 1
+    rebind_i2c("1e78a100");
+    // I2c bus 2
+    rebind_i2c("1e78a180");
+
+    // Set sgpio signals
+    set_gpio("RUN_POWER_EN-O", 1);
+    set_gpio("SYS_RST_IN_L-O", 1);
+    set_gpio("GLOBAL_WP_BMC-O", 0);
+
+    set_gpio("BMC_READY-O", 1);
+
+    if (has_p2020)
+    {
+        init_p2020_gpu_card();
+    }
+
+    set_gpio("USB_HUB_RESET_L-O", 1);
+    if (!hmc_present)
+    {
+        set_gpio("SEC_USB2_HUB_RST_L-O", 1);
+    }
+
+    sd_notify(0, "READY=1");
+    std::cerr << "Platform init complete\n";
+    pause();
+    std::cerr << "Releasing platform\n";
+
+    return EXIT_SUCCESS;
+}
+
+int init_nvidia_gb200_base()
+{
+    return init_nvidia_gb200(false);
+}
+
+int init_nvidia_gb200_with_p2020()
+{
+    return init_nvidia_gb200(true);
+}
+
+constexpr std::array<std::pair<std::string_view, int (*)()>, 2> init_functions{
+    {{"nvidia-gb200", init_nvidia_gb200_base},
+     {"nvidia-gb200-with-p2020", init_nvidia_gb200_with_p2020}}};
+
+int main(int argc, char** argv)
+{
+    CLI::App app("Platform init CLI");
+
+    app.require_subcommand();
+
+    CLI::App* init_sub =
+        app.add_subcommand("init", "Initialize the platform and daemonize");
+    std::string platform_name;
+    init_sub
+        ->add_option("platform_name", platform_name,
+                     "Name of the platform to init")
+        ->required();
+    app.require_subcommand();
+
+    CLI11_PARSE(app, argc, argv)
+
+    const auto* it = std::ranges::find_if(
+        init_functions,
+        [&platform_name](const std::pair<std::string_view, int (*)()> val) {
+            return val.first == platform_name;
+        });
+    if (it == init_functions.end())
+    {
+        std::cerr << init_sub->help() << "\n";
+        return EXIT_FAILURE;
+    }
+
+    return it->second();
+}
diff --git a/platform_init.service.in b/platform_init.service.in
new file mode 100644
index 0000000..5c6d77f
--- /dev/null
+++ b/platform_init.service.in
@@ -0,0 +1,12 @@
+[Unit]
+Description=Start up the platform hardware
+Before=xyz.openbmc_project.FruDevice.service
+
+[Service]
+Type=notify
+ExecStart=/usr/libexec/platform init @PLATFORM_NAME@
+SyslogIdentifier=platform-init
+TimeoutStartSec=5000
+
+[Install]
+WantedBy=multi-user.target
diff --git a/subprojects/cli11.wrap b/subprojects/cli11.wrap
new file mode 100644
index 0000000..dd26d59
--- /dev/null
+++ b/subprojects/cli11.wrap
@@ -0,0 +1,10 @@
+[wrap-file]
+directory = CLI11-2.4.1
+source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.4.1.tar.gz
+source_filename = CLI11-2.4.1.tar.gz
+source_hash = 73b7ec52261ce8fe980a29df6b4ceb66243bb0b779451dbd3d014cfec9fdbb58
+source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cli11_2.4.1-1/CLI11-2.4.1.tar.gz
+wrapdb_version = 2.4.1-1
+
+[provide]
+cli11 = CLI11_dep
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