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',