eeprom device code updater

This commit introduces a code updater for EEPROM-based devices,
such as Retimer connected to an EEPROM.

Key features include:
- Configuring MUX GPIOs to switch EEPROM access to the BMC
- Writing firmware directly to the EEPROM flash memory

The behavior and configuration of this updater are managed via EM,
enabling flexibility and adaptability across different hardware setups.
https://gerrit.openbmc.org/c/openbmc/entity-manager/+/77198

Tested on Harma:

1. Display the fw inventory
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
"@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
"Members": [
    {...},
    {
    "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_9355"
    },
    {...}
],
"Members@odata.count": 26,
"Name": "Software Inventory Collection"
}
```

2. Query Retimer version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_9355
```

```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_9355",
"@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
"Description": "Unknown image",
"Id": "MB_Retimer_9355",
"Name": "Software Inventory",
"Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
},
"Updateable": false,
"Version": "1.31.23"
}
```

3. Trigger the fw update via redfish.
```
curl -k ${creds} \
  -H "Content-Type:multipart/form-data" \
  -X POST \
  -F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_9355\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
  -F "UpdateFile=@${fwpath};type=application/octet-stream" \
  https://${bmc}/redfish/v1/UpdateService/update
```

4. Task is returned

```
{
"@odata.id": "/redfish/v1/TaskService/Tasks/0",
"@odata.type": "#Task.v1_4_3.Task",
"Id": "0",
"TaskState": "Running",
"TaskStatus": "OK"
}
```

5. Query Task status
```
curl --silent $creds https://$bmc/redfish/v1/TaskService/Tasks/0
```

```
{
"@odata.id": "/redfish/v1/TaskService/Tasks/0",
"@odata.type": "#Task.v1_4_3.Task",
"EndTime": "2025-04-17T08:35:58+00:00",
"HidePayload": false,
"Id": "0",
"Messages": [
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has started.",
    "MessageArgs": [
        "0"
    ],
    "MessageId": "TaskEvent.1.0.TaskStarted",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has changed to progress 20 percent complete.",
    "MessageArgs": [
        "0",
        "20"
    ],
    "MessageId": "TaskEvent.1.0.TaskProgressChanged",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has changed to progress 40 percent complete.",
    "MessageArgs": [
        "0",
        "40"
    ],
    "MessageId": "TaskEvent.1.0.TaskProgressChanged",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has changed to progress 60 percent complete.",
    "MessageArgs": [
        "0",
        "60"
    ],
    "MessageId": "TaskEvent.1.0.TaskProgressChanged",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has changed to progress 80 percent complete.",
    "MessageArgs": [
        "0",
        "80"
    ],
    "MessageId": "TaskEvent.1.0.TaskProgressChanged",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has changed to progress 100 percent complete.",
    "MessageArgs": [
        "0",
        "100"
    ],
    "MessageId": "TaskEvent.1.0.TaskProgressChanged",
    "MessageSeverity": "OK",
    "Resolution": "None."
    },
    {
    "@odata.type": "#Message.v1_1_1.Message",
    "Message": "The task with Id '0' has completed.",
    "MessageArgs": [
        "0"
    ],
    "MessageId": "TaskEvent.1.0.TaskCompletedOK",
    "MessageSeverity": "OK",
    "Resolution": "None."
    }
],
"Name": "Task 0",
"Payload": {
    "HttpHeaders": [],
    "HttpOperation": "POST",
    "JsonBody": "null",
    "TargetUri": "/redfish/v1/UpdateService/update"
},
"PercentComplete": 100,
"StartTime": "2025-04-17T08:35:12+00:00",
"TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
"TaskState": "Completed",
"TaskStatus": "OK"
}
```

6. Display the fw inventory with newly updated fw.
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
"@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
"Members": [
    {...},
    {
    "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_5686"
    },
    {...}
],
"Members@odata.count": 26,
"Name": "Software Inventory Collection"
}
```

7. Query the new fw version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_5686
```

```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/MB_Retimer_5686",
"@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
"Description": "Unknown image",
"Id": "MB_Retimer_5686",
"Name": "Software Inventory",
"Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
},
"Updateable": false,
"Version": "2.8.19"
}
```

Change-Id: Ic09f9289fa16d5df738e792348c0c817a15cb808
Signed-off-by: Kevin Tung <Kevin.Tung@quantatw.com>
diff --git a/eeprom-device/eeprom_device.cpp b/eeprom-device/eeprom_device.cpp
new file mode 100644
index 0000000..86f59d9
--- /dev/null
+++ b/eeprom-device/eeprom_device.cpp
@@ -0,0 +1,468 @@
+#include "eeprom_device.hpp"
+
+#include "common/include/software.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <filesystem>
+#include <fstream>
+
+PHOSPHOR_LOG2_USING;
+
+namespace fs = std::filesystem;
+namespace MatchRules = sdbusplus::bus::match::rules;
+namespace State = sdbusplus::common::xyz::openbmc_project::state;
+
+static std::vector<std::unique_ptr<::gpiod::line_bulk>> requestMuxGPIOs(
+    const std::vector<std::string>& gpioLines,
+    const std::vector<bool>& gpioPolarities, bool inverted)
+{
+    std::map<std::string, std::vector<std::string>> groupLineNames;
+    std::map<std::string, std::vector<int>> groupValues;
+
+    for (size_t i = 0; i < gpioLines.size(); ++i)
+    {
+        auto line = ::gpiod::find_line(gpioLines[i]);
+
+        if (!line)
+        {
+            error("Failed to find GPIO line: {LINE}", "LINE", gpioLines[i]);
+            return {};
+        }
+
+        if (line.is_used())
+        {
+            error("GPIO line {LINE} was still used", "LINE", gpioLines[i]);
+            return {};
+        }
+
+        std::string chipName = line.get_chip().name();
+        groupLineNames[chipName].push_back(gpioLines[i]);
+        groupValues[chipName].push_back(gpioPolarities[i] ^ inverted ? 1 : 0);
+    }
+
+    std::vector<std::unique_ptr<::gpiod::line_bulk>> lineBulks;
+    ::gpiod::line_request config{"", ::gpiod::line_request::DIRECTION_OUTPUT,
+                                 0};
+
+    for (auto& [chipName, lineNames] : groupLineNames)
+    {
+        ::gpiod::chip chip(chipName);
+        std::vector<::gpiod::line> lines;
+
+        for (size_t i = 0; i < lineNames.size(); ++i)
+        {
+            const auto& name = lineNames[i];
+            auto line = chip.find_line(name);
+
+            if (!line)
+            {
+                error("Failed to get {LINE} from chip {CHIP}", "LINE", name,
+                      "CHIP", chipName);
+                return {};
+            }
+
+            debug("Requesting chip {CHIP}, GPIO line {LINE} to {VALUE}", "CHIP",
+                  chip.name(), "LINE", line.name(), "VALUE",
+                  groupValues[chipName][i]);
+
+            lines.push_back(std::move(line));
+        }
+
+        auto lineBulk = std::make_unique<::gpiod::line_bulk>(lines);
+
+        if (!lineBulk)
+        {
+            error("Failed to create line bulk for chip={CHIP}", "CHIP",
+                  chipName);
+            return {};
+        }
+
+        lineBulk->request(config, groupValues[chipName]);
+
+        lineBulks.push_back(std::move(lineBulk));
+    }
+
+    return lineBulks;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> asyncSystem(sdbusplus::async::context& ctx,
+                                        const std::string& cmd)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    int pipefd[2];
+    if (pipe(pipefd) == -1)
+    {
+        perror("pipe");
+        co_return -1;
+    }
+
+    pid_t pid = fork();
+    if (pid == -1)
+    {
+        perror("fork");
+        close(pipefd[0]);
+        close(pipefd[1]);
+        co_return -1;
+    }
+    else if (pid == 0)
+    {
+        close(pipefd[0]);
+        int exitCode = std::system(cmd.c_str());
+
+        ssize_t status = write(pipefd[1], &exitCode, sizeof(exitCode));
+        close(pipefd[1]);
+        exit((status == sizeof(exitCode)) ? 0 : 1);
+    }
+    else
+    {
+        close(pipefd[1]);
+
+        sdbusplus::async::fdio pipe_fdio(ctx, pipefd[0]);
+
+        co_await pipe_fdio.next();
+
+        int status;
+        waitpid(pid, &status, 0);
+        close(pipefd[0]);
+
+        co_return WEXITSTATUS(status);
+    }
+}
+
+static std::string getDriverPath(const std::string& chipModel)
+{
+    // Currently, only EEPROM chips with the model AT24 are supported.
+    if (chipModel.find("EEPROM_24C") == std::string::npos)
+    {
+        error("Invalid EEPROM chip model: {CHIP}", "CHIP", chipModel);
+        return "";
+    }
+
+    std::string path = "/sys/bus/i2c/drivers/at24";
+    return std::filesystem::exists(path) ? path : "";
+}
+
+static std::string getI2CDeviceId(const uint16_t bus, const uint8_t address)
+{
+    std::ostringstream oss;
+    oss << bus << "-" << std::hex << std::setfill('0') << std::setw(4)
+        << static_cast<int>(address);
+    return oss.str();
+}
+
+static std::string getEEPROMPath(const uint16_t bus, const uint8_t address)
+{
+    std::string devicePath =
+        "/sys/bus/i2c/devices/" + getI2CDeviceId(bus, address) + "/eeprom";
+
+    if (fs::exists(devicePath) && fs::is_regular_file(devicePath))
+    {
+        debug("Found EEPROM device path: {PATH}", "PATH", devicePath);
+        return devicePath;
+    }
+
+    return "";
+}
+
+EEPROMDevice::EEPROMDevice(
+    sdbusplus::async::context& ctx, const uint16_t bus, const uint8_t address,
+    const std::string& chipModel, const std::vector<std::string>& gpioLines,
+    const std::vector<bool>& gpioPolarities,
+    std::unique_ptr<DeviceVersion> deviceVersion, SoftwareConfig& config,
+    ManagerInf::SoftwareManager* parent) :
+    Device(ctx, config, parent,
+           {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset}),
+    bus(bus), address(address), chipModel(chipModel), gpioLines(gpioLines),
+    gpioPolarities(gpioPolarities), deviceVersion(std::move(deviceVersion)),
+    hostPower(ctx)
+{
+    // Some EEPROM devices require the host to be in a specific state before
+    // retrieving the version. To handle this, set up a match to listen for
+    // property changes on the host state. Once the host reaches the required
+    // condition, the version can be updated accordingly.
+    ctx.spawn(processHostStateChange());
+
+    debug("Initialized EEPROM device instance on dbus");
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> EEPROMDevice::updateDevice(const uint8_t* image,
+                                                        size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::vector<std::unique_ptr<::gpiod::line_bulk>> lineBulks;
+
+    if (!gpioLines.empty())
+    {
+        debug("Requesting GPIOs to mux EEPROM to BMC");
+
+        lineBulks = requestMuxGPIOs(gpioLines, gpioPolarities, false);
+
+        if (lineBulks.empty())
+        {
+            error("Failed to mux EEPROM to BMC");
+            co_return false;
+        }
+    }
+
+    setUpdateProgress(20);
+
+    if (!co_await bindEEPROM())
+    {
+        co_return false;
+    }
+
+    setUpdateProgress(40);
+
+    const int rc = co_await writeEEPROM(image, image_size);
+    if (rc != 0)
+    {
+        error("Error writing to EEPROM, exit code {CODE}", "CODE", rc);
+    }
+
+    bool success = (rc == 0);
+
+    if (success)
+    {
+        debug("Successfully wrote EEPROM");
+        setUpdateProgress(60);
+    }
+    else
+    {
+        error("Failed to write EEPROM");
+    }
+
+    success = success && co_await unbindEEPROM();
+
+    if (success)
+    {
+        setUpdateProgress(80);
+    }
+
+    if (!gpioLines.empty())
+    {
+        for (auto& lineBulk : lineBulks)
+        {
+            lineBulk->release();
+        }
+
+        debug("Requesting GPIOs to mux EEPROM back to device");
+
+        lineBulks = requestMuxGPIOs(gpioLines, gpioPolarities, true);
+
+        if (lineBulks.empty())
+        {
+            error("Failed to mux EEPROM back to device");
+            co_return false;
+        }
+
+        for (auto& lineBulk : lineBulks)
+        {
+            lineBulk->release();
+        }
+    }
+
+    if (success)
+    {
+        debug("EEPROM device successfully updated");
+        setUpdateProgress(100);
+    }
+    else
+    {
+        error("Failed to update EEPROM device");
+    }
+
+    co_return success;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> EEPROMDevice::bindEEPROM()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto i2cDeviceId = getI2CDeviceId(bus, address);
+
+    debug("Binding {I2CDEVICE} EEPROM", "I2CDEVICE", i2cDeviceId);
+
+    if (isEEPROMBound())
+    {
+        debug("EEPROM was already bound, unbinding it now");
+        if (!co_await unbindEEPROM())
+        {
+            error("Error unbinding EEPROM");
+            co_return false;
+        }
+    }
+
+    auto driverPath = getDriverPath(chipModel);
+    if (driverPath.empty())
+    {
+        error("Driver path not found for chip model: {CHIP}", "CHIP",
+              chipModel);
+        co_return false;
+    }
+
+    auto bindPath = driverPath + "/bind";
+    std::ofstream ofbind(bindPath, std::ofstream::out);
+    if (!ofbind)
+    {
+        error("Failed to open bind file: {PATH}", "PATH", bindPath);
+        co_return false;
+    }
+
+    ofbind << i2cDeviceId;
+    ofbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    auto bound = isEEPROMBound();
+    if (!bound)
+    {
+        error("Failed to bind {I2CDEVICE} EEPROM", "I2CDEVICE", i2cDeviceId);
+    }
+
+    co_return bound;
+}
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> EEPROMDevice::unbindEEPROM()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto i2cDeviceId = getI2CDeviceId(bus, address);
+
+    debug("Unbinding EEPROM device {I2CDEVICE}", "I2CDEVICE", i2cDeviceId);
+
+    auto driverPath = getDriverPath(chipModel);
+    if (driverPath.empty())
+    {
+        error("Failed to unbind EEPROM, driver path not found for chip {CHIP}",
+              "CHIP", chipModel);
+        co_return false;
+    }
+
+    auto unbindPath = driverPath + "/unbind";
+    std::ofstream ofunbind(unbindPath, std::ofstream::out);
+    if (!ofunbind)
+    {
+        error("Failed to open unbind file: {PATH}", "PATH", unbindPath);
+        co_return false;
+    }
+    ofunbind << i2cDeviceId;
+    ofunbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    auto bound = isEEPROMBound();
+    if (bound)
+    {
+        error("Failed to unbind {I2CDEVICE} EEPROM", "I2CDEVICE", i2cDeviceId);
+    }
+
+    co_return !bound;
+}
+
+bool EEPROMDevice::isEEPROMBound()
+{
+    auto driverPath = getDriverPath(chipModel);
+
+    if (driverPath.empty())
+    {
+        error("Failed to check if EEPROM is bound");
+        return false;
+    }
+
+    auto i2cDeviceId = getI2CDeviceId(bus, address);
+
+    return std::filesystem::exists(driverPath + "/" + i2cDeviceId);
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> EEPROMDevice::writeEEPROM(const uint8_t* image,
+                                                      size_t image_size) const
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto eepromPath = getEEPROMPath(bus, address);
+    if (eepromPath.empty())
+    {
+        error("EEPROM file not found for device: {DEVICE}", "DEVICE",
+              getI2CDeviceId(bus, address));
+        co_return -1;
+    }
+    const std::string path =
+        "/tmp/eeprom-image-" +
+        std::to_string(SoftwareInf::Software::getRandomId()) + ".bin";
+
+    int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
+    if (fd < 0)
+    {
+        error("Failed to open file: {PATH}", "PATH", path);
+        co_return -1;
+    }
+
+    const ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != image_size)
+    {
+        error("Failed to write image to file");
+        co_return -1;
+    }
+
+    debug("Wrote {SIZE} bytes to {PATH}", "SIZE", bytesWritten, "PATH", path);
+
+    std::string cmd = "dd if=" + path + " of=" + eepromPath + " bs=1k";
+
+    debug("Running {CMD}", "CMD", cmd);
+
+    const int exitCode = co_await asyncSystem(ctx, cmd);
+
+    std::filesystem::remove(path);
+
+    co_return exitCode;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<> EEPROMDevice::processHostStateChange()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto requiredHostState = deviceVersion->getHostStateToQueryVersion();
+
+    if (!requiredHostState)
+    {
+        error("Failed to get required host state");
+        co_return;
+    }
+
+    while (!ctx.stop_requested())
+    {
+        auto [interfaceName, changedProperties] =
+            co_await hostPower.stateChangedMatch
+                .next<std::string,
+                      std::map<std::string, std::variant<std::string>>>();
+
+        auto it = changedProperties.find("CurrentHostState");
+        if (it != changedProperties.end())
+        {
+            const auto& currentHostState = std::get<std::string>(it->second);
+
+            if (currentHostState ==
+                State::convertForMessage(*requiredHostState))
+            {
+                debug("Host state {STATE} matches to retrieve the version",
+                      "STATE", currentHostState);
+                std::string version = deviceVersion->getVersion();
+                if (!version.empty())
+                {
+                    softwareCurrent->setVersion(version);
+                }
+            }
+        }
+    }
+
+    co_return;
+}