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_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;
+}