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/README.md b/eeprom-device/README.md
new file mode 100644
index 0000000..a0e6a68
--- /dev/null
+++ b/eeprom-device/README.md
@@ -0,0 +1,48 @@
+# EEPROM Device Update Daemon
+
+This daemon implements the update process for EEPROM device attached via I2C.
+
+## Entity Manager Configuration Example (Harma)
+
+The following JSON snippet demonstrates how to configure an EEPROM device,
+including bus details, address, GPIO settings, and compatibility information.
+
+```json
+{
+  "Name": "MB_Retimer",
+  "Bus": 12,
+  "Address": "0x24",
+  "FirmwareDevice": "MB_Retimer_EEPROM",
+  "MuxOutputs": [
+    {
+      "Name": "rt-cpu0-p1-enable",
+      "Polarity": "High"
+    },
+    {
+      "Name": "smb-rt-rom-p1-select",
+      "Polarity": "High"
+    }
+  ],
+  "FirmwareInfo": {
+    "VendorIANA": 40981,
+    "CompatibleHardware": "com.meta.Hardware.Harma.pt5161l.Retimer"
+  },
+  "Type": "PT5161L"
+}
+```
+
+## Entity Manager Interface
+
+The EEPROM device configuration can be found at the following D-Bus path,
+provided that the EEPROMDevice is a expose record in the Harma_MB Entity Manager
+configuration:
+
+```bash
+/xyz/openbmc_project/inventory/system/board/Harma_MB/MB_Retimer
+```
+
+The D-Bus interface name for EEPROMDevice configuration will be as follows:
+
+```bash
+xyz.openbmc_project.Configuration.EEPROMDevice
+```
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;
+}
diff --git a/eeprom-device/eeprom_device.hpp b/eeprom-device/eeprom_device.hpp
new file mode 100644
index 0000000..6e6eee5
--- /dev/null
+++ b/eeprom-device/eeprom_device.hpp
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/host_power.hpp"
+#include "common/include/software.hpp"
+#include "common/include/software_manager.hpp"
+#include "eeprom_device_version.hpp"
+
+#include <gpiod.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/bus/match.hpp>
+
+#include <string>
+
+namespace SoftwareInf = phosphor::software;
+namespace ManagerInf = SoftwareInf::manager;
+namespace HostPowerInf = SoftwareInf::host_power;
+
+class EEPROMDevice : public Device
+{
+  public:
+    EEPROMDevice(sdbusplus::async::context& ctx, uint16_t bus, 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);
+
+    using Device::softwareCurrent;
+
+    sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                              size_t image_size) final;
+
+  private:
+    uint16_t bus;
+    uint8_t address;
+    std::string chipModel;
+    std::vector<std::string> gpioLines;
+    std::vector<bool> gpioPolarities;
+    std::unique_ptr<DeviceVersion> deviceVersion;
+    HostPowerInf::HostPower hostPower;
+
+    /**
+     * @brief Binds the EEPROM device driver to the I2C device.
+     *
+     * @return `true` on success, `false` otherwise.
+     */
+    sdbusplus::async::task<bool> bindEEPROM();
+    /**
+     * @brief Unbinds the EEPROM device driver from the I2C device.
+     *
+     * @return `true` on success, `false` otherwise.
+     */
+    sdbusplus::async::task<bool> unbindEEPROM();
+    /**
+     * @brief Checks if the EEPROM device is currently bound to its driver.
+     *
+     * @return `true` if the EEPROM device is bound, `false` otherwise.
+     */
+    bool isEEPROMBound();
+    /**
+     * @brief Writes data to the EEPROM.
+     *
+     * @param image         - Pointer to the data to write.
+     * @param image_size    - Size of the data to write in bytes.
+     * @return `true` on success, `false` otherwise.
+     */
+    sdbusplus::async::task<int> writeEEPROM(const uint8_t* image,
+                                            size_t image_size) const;
+    /**
+     *  @brief Handle async host state change signal and updates the version.
+     */
+    sdbusplus::async::task<> processHostStateChange();
+};
diff --git a/eeprom-device/eeprom_device_software_manager.cpp b/eeprom-device/eeprom_device_software_manager.cpp
new file mode 100644
index 0000000..b29cf82
--- /dev/null
+++ b/eeprom-device/eeprom_device_software_manager.cpp
@@ -0,0 +1,198 @@
+#include "eeprom_device_software_manager.hpp"
+
+#include "common/include/dbus_helper.hpp"
+#include "eeprom_device.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+#include <fstream>
+#include <optional>
+#include <sstream>
+
+PHOSPHOR_LOG2_USING;
+
+namespace SoftwareInf = phosphor::software;
+
+const std::vector<std::string> emConfigTypes = {"PT5161L"};
+
+void EEPROMDeviceSoftwareManager::start()
+{
+    std::vector<std::string> configIntfs;
+    configIntfs.reserve(emConfigTypes.size());
+
+    std::transform(emConfigTypes.begin(), emConfigTypes.end(),
+                   std::back_inserter(configIntfs),
+                   [](const std::string& type) {
+                       return "xyz.openbmc_project.Configuration." + type;
+                   });
+
+    ctx.spawn(initDevices(configIntfs));
+    ctx.run();
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> EEPROMDeviceSoftwareManager::initDevice(
+    const std::string& service, const std::string& path, SoftwareConfig& config)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    const std::string configIface =
+        "xyz.openbmc_project.Configuration." + config.configType;
+
+    std::optional<uint64_t> bus = co_await dbusGetRequiredProperty<uint64_t>(
+        ctx, service, path, configIface, "Bus");
+
+    std::optional<uint64_t> address =
+        co_await dbusGetRequiredProperty<uint64_t>(ctx, service, path,
+                                                   configIface, "Address");
+
+    std::optional<std::string> type =
+        co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                      configIface, "Type");
+
+    std::optional<std::string> fwDevice =
+        co_await dbusGetRequiredProperty<std::string>(
+            ctx, service, path, configIface, "FirmwareDevice");
+
+    if (!bus.has_value() || !address.has_value() || !type.has_value() ||
+        !fwDevice.has_value())
+    {
+        error("Missing EEPROM device config property");
+        co_return false;
+    }
+
+    debug("EEPROM Device: Bus={BUS}, Address={ADDR}, Type={TYPE}, "
+          "Firmware Device={DEVICE}",
+          "BUS", bus.value(), "ADDR", address.value(), "TYPE", type.value(),
+          "DEVICE", fwDevice.value());
+
+    std::unique_ptr<DeviceVersion> deviceVersion =
+        getVersionProvider(type.value(), bus.value(), address.value());
+
+    if (!deviceVersion)
+    {
+        error("Failed to get version provider for chip type: {CHIP}", "CHIP",
+              type.value());
+        co_return false;
+    }
+
+    std::string version = deviceVersion->getVersion();
+
+    using ObjectMapper =
+        sdbusplus::client::xyz::openbmc_project::ObjectMapper<>;
+
+    auto mapper = ObjectMapper(ctx)
+                      .service(ObjectMapper::default_service)
+                      .path(ObjectMapper::instance_path);
+
+    auto res =
+        co_await mapper.get_sub_tree("/xyz/openbmc_project/inventory", 0, {});
+
+    bus.reset();
+    address.reset();
+    type.reset();
+
+    for (auto& [p, v] : res)
+    {
+        if (!p.ends_with(fwDevice.value()))
+        {
+            continue;
+        }
+
+        for (auto& [s, ifaces] : v)
+        {
+            for (std::string& iface : ifaces)
+            {
+                if (iface.starts_with("xyz.openbmc_project.Configuration."))
+                {
+                    bus = co_await dbusGetRequiredProperty<uint64_t>(
+                        ctx, s, p, iface, "Bus");
+
+                    address = co_await dbusGetRequiredProperty<uint64_t>(
+                        ctx, s, p, iface, "Address");
+
+                    type = co_await dbusGetRequiredProperty<std::string>(
+                        ctx, s, p, iface, "Type");
+                    break;
+                }
+            }
+            if (bus.has_value() && address.has_value() && type.has_value())
+            {
+                break;
+            }
+        }
+        break;
+    }
+
+    if (!bus.has_value() || !address.has_value() || !type.has_value())
+    {
+        error("Missing EEPROM config property");
+        co_return false;
+    }
+
+    debug("EEPROM: Bus={BUS}, Address={ADDR}, Type={TYPE}", "BUS", bus.value(),
+          "ADDR", address.value(), "TYPE", type.value());
+
+    const std::string configIfaceMux = configIface + ".MuxOutputs";
+    std::vector<std::string> gpioLines;
+    std::vector<bool> gpioPolarities;
+
+    for (size_t i = 0; true; i++)
+    {
+        const std::string iface = configIfaceMux + std::to_string(i);
+
+        std::optional<std::string> name =
+            co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                          iface, "Name");
+
+        std::optional<std::string> polarity =
+            co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                          iface, "Polarity");
+
+        if (!name.has_value() || !polarity.has_value())
+        {
+            break;
+        }
+
+        gpioLines.push_back(name.value());
+        gpioPolarities.push_back(polarity.value() == "High");
+    }
+
+    for (size_t i = 0; i < gpioLines.size(); i++)
+    {
+        debug("Mux gpio {NAME} polarity = {VALUE}", "NAME", gpioLines[i],
+              "VALUE", gpioPolarities[i]);
+    }
+
+    auto eepromDevice = std::make_unique<EEPROMDevice>(
+        ctx, static_cast<uint16_t>(bus.value()),
+        static_cast<uint8_t>(address.value()), type.value(), gpioLines,
+        gpioPolarities, std::move(deviceVersion), config, this);
+
+    std::unique_ptr<SoftwareInf::Software> software =
+        std::make_unique<SoftwareInf::Software>(ctx, *eepromDevice);
+
+    software->setVersion(version.empty() ? "Unknown" : version);
+
+    std::set<RequestedApplyTimes> allowedApplyTimes = {
+        RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset};
+
+    software->enableUpdate(allowedApplyTimes);
+
+    eepromDevice->softwareCurrent = std::move(software);
+
+    devices.insert({config.objectPath, std::move(eepromDevice)});
+
+    co_return true;
+}
+
+int main()
+{
+    sdbusplus::async::context ctx;
+
+    EEPROMDeviceSoftwareManager eepromDeviceSoftwareManager(ctx);
+
+    eepromDeviceSoftwareManager.start();
+    return 0;
+}
diff --git a/eeprom-device/eeprom_device_software_manager.hpp b/eeprom-device/eeprom_device_software_manager.hpp
new file mode 100644
index 0000000..b248f8d
--- /dev/null
+++ b/eeprom-device/eeprom_device_software_manager.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "common/include/software_manager.hpp"
+
+namespace ManagerInf = phosphor::software::manager;
+
+const std::string configTypeEEPROMDevice = "EERPOMDevice";
+
+class EEPROMDeviceSoftwareManager : public ManagerInf::SoftwareManager
+{
+  public:
+    EEPROMDeviceSoftwareManager(sdbusplus::async::context& ctx) :
+        SoftwareManager(ctx, configTypeEEPROMDevice)
+    {}
+
+    void start();
+
+    sdbusplus::async::task<bool> initDevice(const std::string& service,
+                                            const std::string& path,
+                                            SoftwareConfig& config) final;
+
+  private:
+    sdbusplus::async::task<bool> getDeviceProperties(
+        const std::string& service, const std::string& path,
+        const std::string& intf, uint16_t& bus, uint8_t& address,
+        std::string& chipModel);
+};
diff --git a/eeprom-device/eeprom_device_version.cpp b/eeprom-device/eeprom_device_version.cpp
new file mode 100644
index 0000000..fa0e238
--- /dev/null
+++ b/eeprom-device/eeprom_device_version.cpp
@@ -0,0 +1,31 @@
+#include "eeprom_device_version.hpp"
+
+#include "pt5161l/pt5161l.hpp"
+
+#include <functional>
+#include <unordered_map>
+
+using ProviderFactory = std::function<std::unique_ptr<DeviceVersion>(
+    const std::string&, const uint16_t, const uint8_t)>;
+
+template <typename ProviderType>
+std::unique_ptr<DeviceVersion> createProvider(
+    const std::string& chipModel, const uint16_t bus, const uint8_t address)
+{
+    return std::make_unique<ProviderType>(chipModel, bus, address);
+}
+
+static const std::unordered_map<std::string, ProviderFactory> providerMap = {
+    {"PT5161L", createProvider<PT5161LDeviceVersion>}};
+
+std::unique_ptr<DeviceVersion> getVersionProvider(
+    const std::string& chipModel, const uint16_t bus, const uint8_t address)
+{
+    auto it = providerMap.find(chipModel);
+    if (it != providerMap.end())
+    {
+        return it->second(chipModel, bus, address);
+    }
+
+    return nullptr;
+}
diff --git a/eeprom-device/eeprom_device_version.hpp b/eeprom-device/eeprom_device_version.hpp
new file mode 100644
index 0000000..b05b2c1
--- /dev/null
+++ b/eeprom-device/eeprom_device_version.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "common/include/host_power.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+
+namespace HostPowerInf = phosphor::software::host_power;
+
+class DeviceVersion
+{
+  public:
+    DeviceVersion(const std::string& chipModel, const uint16_t bus,
+                  const uint8_t address) :
+        chipModel(chipModel), bus(bus), address(address)
+    {}
+
+    virtual std::string getVersion() = 0;
+    virtual std::optional<HostPowerInf::HostState>
+        getHostStateToQueryVersion() = 0;
+
+    virtual ~DeviceVersion() = default;
+    DeviceVersion(const DeviceVersion&) = delete;
+    DeviceVersion& operator=(const DeviceVersion&) = delete;
+    DeviceVersion(DeviceVersion&&) = delete;
+    DeviceVersion& operator=(DeviceVersion&&) = delete;
+
+  protected:
+    std::string chipModel;
+    uint16_t bus;
+    uint8_t address;
+};
+
+std::unique_ptr<DeviceVersion> getVersionProvider(
+    const std::string& chipModel, uint16_t bus, uint8_t address);
diff --git a/eeprom-device/meson.build b/eeprom-device/meson.build
new file mode 100644
index 0000000..2e8edd7
--- /dev/null
+++ b/eeprom-device/meson.build
@@ -0,0 +1,34 @@
+eeprom_device_version_src = files(
+    'eeprom_device_version.cpp',
+    'pt5161l/pt5161l.cpp',
+)
+
+eeprom_device_include = include_directories('.')
+
+libgpiod = dependency(
+    'libgpiodcxx',
+    default_options: ['bindings=cxx'],
+    version: '>=1.1.2',
+)
+
+executable(
+    'phosphor-eepromdevice-software-update',
+    'eeprom_device_software_manager.cpp',
+    'eeprom_device.cpp',
+    eeprom_device_version_src,
+    include_directories: [common_include, eeprom_device_include],
+    dependencies: [sdbusplus_dep, phosphor_logging_dep, libgpiod],
+    link_with: [software_common_lib, libpldmutil],
+    install: true,
+    install_dir: get_option('libexecdir') / 'phosphor-code-mgmt',
+)
+
+systemd_system_unit_dir = dependency('systemd').get_variable(
+    'systemdsystemunitdir',
+    pkgconfig_define: ['prefix', get_option('prefix')],
+)
+
+install_data(
+    'xyz.openbmc_project.Software.EEPROMDevice.service',
+    install_dir: systemd_system_unit_dir,
+)
diff --git a/eeprom-device/pt5161l/pt5161l.cpp b/eeprom-device/pt5161l/pt5161l.cpp
new file mode 100644
index 0000000..2406862
--- /dev/null
+++ b/eeprom-device/pt5161l/pt5161l.cpp
@@ -0,0 +1,46 @@
+#include "pt5161l.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <algorithm>
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+
+PHOSPHOR_LOG2_USING;
+
+std::string PT5161LDeviceVersion::getVersion()
+{
+    std::string version;
+    std::ostringstream busOss;
+    std::ostringstream addrOss;
+
+    busOss << std::setw(2) << std::setfill('0') << static_cast<int>(bus);
+    addrOss << std::setw(4) << std::setfill('0') << std::hex << std::nouppercase
+            << static_cast<int>(address);
+
+    // The PT5161L driver exposes the firmware version through the fw_ver node
+    std::string path = "/sys/kernel/debug/pt5161l/" + busOss.str() + "-" +
+                       addrOss.str() + "/fw_ver";
+
+    std::ifstream file(path);
+    if (!file)
+    {
+        error("Failed to get version: unable to open file: {PATH}", "PATH",
+              path);
+        return version;
+    }
+
+    if (!std::getline(file, version) || version.empty())
+    {
+        error("Failed to read version from file: {PATH}", "PATH", path);
+    }
+
+    return version;
+}
+
+std::optional<HostPowerInf::HostState>
+    PT5161LDeviceVersion::getHostStateToQueryVersion()
+{
+    return HostPowerInf::HostState::Running;
+}
diff --git a/eeprom-device/pt5161l/pt5161l.hpp b/eeprom-device/pt5161l/pt5161l.hpp
new file mode 100644
index 0000000..23cf017
--- /dev/null
+++ b/eeprom-device/pt5161l/pt5161l.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "eeprom-device/eeprom_device_version.hpp"
+
+class PT5161LDeviceVersion : public DeviceVersion
+{
+  public:
+    using DeviceVersion::DeviceVersion;
+    std::string getVersion() final;
+    std::optional<HostPowerInf::HostState> getHostStateToQueryVersion() final;
+};
diff --git a/eeprom-device/xyz.openbmc_project.Software.EEPROMDevice.service b/eeprom-device/xyz.openbmc_project.Software.EEPROMDevice.service
new file mode 100644
index 0000000..5488dad
--- /dev/null
+++ b/eeprom-device/xyz.openbmc_project.Software.EEPROMDevice.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=EEPROM Device Code Update Daemon
+Wants=xyz.openbmc_project.State.Chassis@0.service
+After=xyz.openbmc_project.State.Chassis@0.service
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+ExecStart=/usr/libexec/phosphor-code-mgmt/phosphor-eepromdevice-software-update
+Restart=always
+Type=dbus
+BusName=xyz.openbmc_project.Software.EERPOMDevice
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meson.build b/meson.build
index d785bb4..459421d 100644
--- a/meson.build
+++ b/meson.build
@@ -70,6 +70,8 @@
 
 common_build = build_tests.allowed() or get_option('bios-software-update').enabled() or get_option(
     'i2cvr-software-update',
+).enabled() or get_option(
+    'eepromdevice-software-update',
 ).enabled()
 
 if common_build
@@ -93,6 +95,10 @@
     subdir('i2c-vr')
 endif
 
+if get_option('eepromdevice-software-update').enabled()
+    subdir('eeprom-device')
+endif
+
 if build_tests.allowed()
     subdir('test')
 endif
diff --git a/meson.options b/meson.options
index 157c762..c178e69 100644
--- a/meson.options
+++ b/meson.options
@@ -70,6 +70,13 @@
     description: 'Enable BIOS/Host firmware update',
 )
 
+option(
+    'eepromdevice-software-update',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Enable EEPROM device update support.',
+)
+
 # Variables
 option(
     'active-bmc-max-allowed',