Implement tlbmc led module

PiperOrigin-RevId: 970236748
Change-Id: Ife28857d25a31468fbd0465d5259e80d6fe4d6dc
diff --git a/copy.bara.sky b/copy.bara.sky
index 53c11f6..7046332 100644
--- a/copy.bara.sky
+++ b/copy.bara.sky
@@ -465,6 +465,8 @@
     "google3/third_party/milotic/external/cc/tlbmc/collector/fru_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/gpio_collector.cc",
     "google3/third_party/milotic/external/cc/tlbmc/collector/gpio_collector.h",
+    "google3/third_party/milotic/external/cc/tlbmc/collector/led_collector.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/collector/led_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/metric_collector.cc",
     "google3/third_party/milotic/external/cc/tlbmc/collector/metric_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/monitoring_change_base.h",
@@ -507,6 +509,7 @@
     "google3/third_party/milotic/external/cc/tlbmc/configs/hal_common_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/hwmon_temp_sensor_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/intel_cpu_sensor_config.proto",
+    "google3/third_party/milotic/external/cc/tlbmc/configs/led_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/nic_telemetry_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/pcie_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/power_control.proto",
@@ -609,6 +612,10 @@
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/gpio_monitor.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/on_demand_gpio.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/on_demand_gpio.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led_sysfs.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led_sysfs.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/accessor_base.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/interface.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/interface.h",
@@ -645,6 +652,8 @@
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/i3c.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/iio.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/iio.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/led.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/led.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/peci.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/peci.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/system_registry.h",
@@ -930,6 +939,8 @@
     "tlbmc/collector/fru_collector.h",
     "tlbmc/collector/gpio_collector.cc",
     "tlbmc/collector/gpio_collector.h",
+    "tlbmc/collector/led_collector.cc",
+    "tlbmc/collector/led_collector.h",
     "tlbmc/collector/metric_collector.cc",
     "tlbmc/collector/metric_collector.h",
     "tlbmc/collector/monitoring_change_base.h",
@@ -1006,6 +1017,10 @@
     "tlbmc/hal/gpio/gpio_monitor.h",
     "tlbmc/hal/gpio/on_demand_gpio.cc",
     "tlbmc/hal/gpio/on_demand_gpio.h",
+    "tlbmc/hal/led/led.cc",
+    "tlbmc/hal/led/led.h",
+    "tlbmc/hal/led/led_sysfs.cc",
+    "tlbmc/hal/led/led_sysfs.h",
     "tlbmc/hal/nic_veeprom/accessor_base.h",
     "tlbmc/hal/nic_veeprom/interface.cc",
     "tlbmc/hal/nic_veeprom/interface.h",
@@ -1038,6 +1053,8 @@
     "tlbmc/hal/sysfs/i3c.h",
     "tlbmc/hal/sysfs/iio.cc",
     "tlbmc/hal/sysfs/iio.h",
+    "tlbmc/hal/sysfs/led.cc",
+    "tlbmc/hal/sysfs/led.h",
     "tlbmc/hal/sysfs/peci.cc",
     "tlbmc/hal/sysfs/peci.h",
     "tlbmc/hal_common_config.proto",
@@ -1061,6 +1078,7 @@
     "tlbmc/hwmon_temp_sensor_config.proto",
     "tlbmc/identifier.proto",
     "tlbmc/intel_cpu_sensor_config.proto",
+    "tlbmc/led_config.proto",
     "tlbmc/memory.proto",
     "tlbmc/meson.build",
     "tlbmc/metrics/bmc_static_metrics.cc",
@@ -1282,6 +1300,8 @@
     "google3/third_party/milotic/external/cc/tlbmc/collector/fru_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/gpio_collector.cc",
     "google3/third_party/milotic/external/cc/tlbmc/collector/gpio_collector.h",
+    "google3/third_party/milotic/external/cc/tlbmc/collector/led_collector.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/collector/led_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/metric_collector.cc",
     "google3/third_party/milotic/external/cc/tlbmc/collector/metric_collector.h",
     "google3/third_party/milotic/external/cc/tlbmc/collector/monitoring_change_base.h",
@@ -1318,6 +1338,7 @@
     "google3/third_party/milotic/external/cc/tlbmc/configs/hal_common_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/hwmon_temp_sensor_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/intel_cpu_sensor_config.proto",
+    "google3/third_party/milotic/external/cc/tlbmc/configs/led_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/nic_telemetry_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/pcie_config.proto",
     "google3/third_party/milotic/external/cc/tlbmc/configs/power_control.proto",
@@ -1371,6 +1392,10 @@
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/gpio_monitor.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/on_demand_gpio.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/gpio/on_demand_gpio.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led_sysfs.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/led/led_sysfs.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/accessor_base.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/interface.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/nic_veeprom/interface.h",
@@ -1406,6 +1431,8 @@
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/i3c.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/iio.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/iio.h",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/led.cc",
+    "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/led.h",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/peci.cc",
     "google3/third_party/milotic/external/cc/tlbmc/hal/sysfs/peci.h",
     "google3/third_party/milotic/external/cc/tlbmc/hft/hft_service.cc",
@@ -2655,6 +2682,7 @@
     "tlbmc/configs/gpio_sensor_config.proto.h": "gpio_sensor_config.pb.h",
     "tlbmc/configs/thermal_config.proto.h": "thermal_config.pb.h",
     "tlbmc/configs/thermal_controller_specification.proto.h": "thermal_controller_specification.pb.h",
+    "tlbmc/configs/led_config.proto.h": "led_config.pb.h",
     "tlbmc/types/payload.proto.h": "payload.pb.h",
     "tlbmc/types/sensor_payload.proto.h": "sensor_payload.pb.h",
     "tlbmc/types/subscription_params.proto.h": "subscription_params.pb.h",
@@ -3592,6 +3620,7 @@
     "gpio_fru_config.pb.h": "third_party/milotic/external/cc/tlbmc/configs/gpio_fru_config.proto.h",
     "usb_fru_config.pb.h": "third_party/milotic/external/cc/tlbmc/configs/usb_fru_config.proto.h",
     "gpio_sensor_config.pb.h": "third_party/milotic/external/cc/tlbmc/configs/gpio_sensor_config.proto.h",
+    "led_config.pb.h": "third_party/milotic/external/cc/tlbmc/configs/led_config.proto.h",
     "hal_common_config.pb.h": "third_party/milotic/external/cc/tlbmc/configs/hal_common_config.proto.h",
     "hft_capabilities.pb.h": "third_party/milotic/external/cc/hft/service/hft_capabilities.proto.h",
     "hft_service.pb.h": "third_party/milotic/external/cc/hft/service/hft_service.proto.h",
diff --git a/g3/server.cc b/g3/server.cc
index f4e3489..f1fad79 100644
--- a/g3/server.cc
+++ b/g3/server.cc
@@ -27,6 +27,7 @@
 #include <vector>
 
 #include "tlbmc/credentials/credential_manager.h"
+#include "tlbmc/hal/sysfs/led.h"
 
 #pragma GCC diagnostic push
 #pragma GCC diagnostic warning \
@@ -1130,6 +1131,7 @@
   using ::milotic_tlbmc::I3cSysfs;
   using ::milotic_tlbmc::IioSysfs;
   using ::milotic_tlbmc::InitializeTlbmcConfig;
+  using ::milotic_tlbmc::LedSysfs;
   using ::milotic_tlbmc::PeciSysfs;
   using ::milotic_tlbmc::RateLimiterConfig;
   using ::milotic_tlbmc::RedfishApp;
@@ -1420,6 +1422,8 @@
       .i2c_sysfs_path = config.tlbmc_i2c_sysfs_path});
   options.i3c_sysfs = std::make_unique<I3cSysfs>(milotic_tlbmc::I3cSysfsConfig{
       .i3c_sysfs_path = config.tlbmc_i3c_sysfs_path});
+  options.led_sysfs = std::make_unique<LedSysfs>(milotic_tlbmc::LedSysfsConfig{
+      .led_sysfs_path = config.tlbmc_led_sysfs_path});
   options.peci_sysfs =
       std::make_unique<PeciSysfs>(milotic_tlbmc::PeciSysfsConfig{
           .peci_sysfs_path = config.tlbmc_peci_sysfs_path});
diff --git a/g3/server.h b/g3/server.h
index 4fe2305..35840c0 100644
--- a/g3/server.h
+++ b/g3/server.h
@@ -92,6 +92,7 @@
   std::string tlbmc_iio_sysfs_path = "/sys/bus/iio/devices/";
   std::string tlbmc_i2c_sysfs_path = "/sys/bus/i2c/devices/";
   std::string tlbmc_i3c_sysfs_path = "/sys/bus/i3c/devices/";
+  std::string tlbmc_led_sysfs_path = "/sys/class/leds/";
   std::string tlbmc_peci_sysfs_path = "/sys/bus/peci";
   std::string tlbmc_root_dir = "/";
 
diff --git a/tlbmc/central_config.proto b/tlbmc/central_config.proto
index f9d86da..eb88e02 100644
--- a/tlbmc/central_config.proto
+++ b/tlbmc/central_config.proto
@@ -226,6 +226,11 @@
   bool enabled = 1 [default = false];
 }
 
+// Controls the LED collector module.
+message LedCollectorModule {
+  bool enabled = 1 [default = false];
+}
+
 // A proto message to hold all configurations of the modules in tlbmc.
 message TlbmcConfig {
   string platform_name = 8;
@@ -240,6 +245,7 @@
   PowerFaultLogCollectorModule power_fault_log_collector_module = 9;
   SelCollectorModule sel_collector_module = 10;
   PcieCollectorModule pcie_collector_module = 11;
+  LedCollectorModule led_collector_module = 14;
 
   // If true, this platform uses the TLBMC Lite configuration as a base.
   bool use_tlbmc_lite_config = 12;
diff --git a/tlbmc/collector/led_collector.cc b/tlbmc/collector/led_collector.cc
new file mode 100644
index 0000000..9106f96
--- /dev/null
+++ b/tlbmc/collector/led_collector.cc
@@ -0,0 +1,167 @@
+#include "tlbmc/collector/led_collector.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/time/time.h"
+#include "g3/macros.h"
+#include <nlohmann/json.hpp>
+#include "led_config.pb.h"
+#include "tlbmc/hal/led/led.h"
+#include "tlbmc/hal/led/led_sysfs.h"
+#include "tlbmc/scheduler/scheduler.h"
+
+namespace milotic_tlbmc {
+
+absl::StatusOr<std::unique_ptr<LedCollector>> LedCollector::Create(
+    const Params& params) {
+  auto task_scheduler = std::make_unique<TaskScheduler>(params.clock);
+
+  absl::flat_hash_map<std::string, std::unique_ptr<Led>> leds;
+  for (const LedConfig& config : params.led_configs.leds()) {
+    auto [it, inserted] = leds.try_emplace(config.name(), nullptr);
+    if (!inserted) {
+      return absl::AlreadyExistsError(
+          absl::StrCat("Duplicate LED name: ", config.name()));
+    }
+
+    switch (config.output().target_case()) {
+      case LedOutput::TargetCase::kSysfsName: {
+        ECCLESIA_ASSIGN_OR_RETURN(std::unique_ptr<Led> led,
+                                  SysfsLed::Create(config, params.led_sysfs));
+        it->second = std::move(led);
+        break;
+      }
+        // TODO b/541951164 - Add GPIO support.
+      default:
+        return absl::InvalidArgumentError(
+            absl::StrCat("Unsupported output type for LED ", config.name()));
+    }
+
+    ECCLESIA_RETURN_IF_ERROR(
+        it->second->SetToDefaultBehavior(params.clock->Now()));
+  }
+
+  absl::flat_hash_map<std::string, std::vector<LedPresetBehavior>> presets;
+  for (const LedPreset& preset : params.led_configs.presets()) {
+    if (preset.name().empty()) {
+      return absl::InvalidArgumentError("Preset name cannot be empty");
+    }
+    std::vector<LedPresetBehavior> preset_behaviors;
+    preset_behaviors.reserve(preset.members_size());
+    for (const LedPresetMember& member : preset.members()) {
+      auto led_it = leds.find(member.name());
+      if (led_it == leds.end()) {
+        return absl::NotFoundError(
+            absl::StrCat("LED in preset not found: ", member.name()));
+      }
+      preset_behaviors.push_back({*(led_it->second), member.behavior()});
+    }
+    auto [_, inserted] =
+        presets.try_emplace(preset.name(), std::move(preset_behaviors));
+    if (!inserted) {
+      return absl::AlreadyExistsError(
+          absl::StrCat("Duplicate preset name: ", preset.name()));
+    }
+  }
+
+  return std::unique_ptr<LedCollector>(new LedCollector(
+      std::move(task_scheduler), std::move(leds), std::move(presets)));
+}
+
+absl::Status LedCollector::SetLedBehavior(absl::string_view led_name,
+                                          const LedBehavior& behavior,
+                                          absl::Time timestamp) {
+  auto it = leds_.find(led_name);
+  if (it == leds_.end()) {
+    return absl::NotFoundError(absl::StrCat("LED not found: ", led_name));
+  }
+  return it->second->SetBehavior(behavior, timestamp);
+}
+
+absl::StatusOr<LedBehavior> LedCollector::GetLedBehavior(
+    absl::string_view led_name) const {
+  auto it = leds_.find(led_name);
+  if (it == leds_.end()) {
+    return absl::NotFoundError(absl::StrCat("LED not found: ", led_name));
+  }
+  return it->second->GetBehavior();
+}
+
+absl::Status LedCollector::ApplyPreset(absl::string_view preset_name,
+                                       absl::Time timestamp) {
+  auto it = presets_.find(preset_name);
+  if (it == presets_.end()) {
+    return absl::NotFoundError(absl::StrCat("Preset not found: ", preset_name));
+  }
+  absl::Status overall_status = absl::OkStatus();
+  for (const LedPresetBehavior& member : it->second) {
+    overall_status.Update(member.led.SetBehavior(member.behavior, timestamp));
+  }
+  return overall_status;
+}
+
+std::vector<std::string> LedCollector::GetLedNames() const {
+  std::vector<std::string> names;
+  names.reserve(leds_.size());
+  for (const auto& [name, _] : leds_) {
+    names.push_back(name);
+  }
+  return names;
+}
+
+std::vector<std::string> LedCollector::GetPresetNames() const {
+  std::vector<std::string> names;
+  names.reserve(presets_.size());
+  for (const auto& [name, _] : presets_) {
+    names.push_back(name);
+  }
+  return names;
+}
+
+nlohmann::json LedCollector::ToJson() const {
+  nlohmann::json response;
+
+  nlohmann::json leds = nlohmann::json::object();
+  for (const auto& [name, led] : leds_) {
+    leds[name] = led->ToJson();
+  }
+  response["leds"] = leds;
+
+  nlohmann::json presets = nlohmann::json::object();
+  for (const auto& [name, preset] : presets_) {
+    nlohmann::json preset_json = nlohmann::json::array();
+    for (const LedPresetBehavior& member : preset) {
+      preset_json.push_back({
+          {"name", member.led.GetName()},
+          {"behavior", Led::BehaviorToJson(member.behavior)},
+      });
+    }
+    presets[name] = preset_json;
+  }
+  response["presets"] = presets;
+
+  return response;
+}
+
+nlohmann::json EmptyLedCollector::GetSchedulerStats() const {
+  return nlohmann::json::parse("{\"Warning\": \"EmptyLedCollector used.\"}");
+}
+
+nlohmann::json EmptyLedCollector::ToJson() const {
+  return nlohmann::json::parse("{\"Warning\": \"EmptyLedCollector used.\"}");
+}
+
+std::unique_ptr<EmptyLedCollector> EmptyLedCollector::Create() {
+  return std::make_unique<EmptyLedCollector>();
+}
+
+}  // namespace milotic_tlbmc
diff --git a/tlbmc/collector/led_collector.h b/tlbmc/collector/led_collector.h
new file mode 100644
index 0000000..8a1050b
--- /dev/null
+++ b/tlbmc/collector/led_collector.h
@@ -0,0 +1,111 @@
+#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_COLLECTOR_LED_COLLECTOR_H_
+#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_COLLECTOR_LED_COLLECTOR_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "absl/time/time.h"
+#include "time/clock.h"
+#include <nlohmann/json.hpp>
+#include "tlbmc/collector/collector.h"
+#include "led_config.pb.h"
+#include "tlbmc/hal/led/led.h"
+#include "tlbmc/hal/sysfs/led.h"
+#include "tlbmc/scheduler/scheduler.h"
+
+namespace milotic_tlbmc {
+
+struct LedPresetBehavior {
+  Led& led;
+  LedBehavior behavior;
+};
+
+// Collector for managing LEDs and applying presets in tlBMC.
+//
+// The collector owns a set of generic Led (see hal/led/led.h), keyed by their
+// configured name, and a set of named presets. LEDs are created via
+// hardware-backend factory methods (such as SysfsLed::Create()).
+// The collector is agnostic to the underlying hardware.
+class LedCollector : public Collector {
+ public:
+  struct Params {
+    LedConfigs led_configs;
+    const LedSysfs& led_sysfs;
+    ecclesia::Clock* clock = ecclesia::Clock::RealClock();
+  };
+
+  static absl::StatusOr<std::unique_ptr<LedCollector>> Create(
+      const Params& params);
+
+  ~LedCollector() override {
+    // Destroy the scheduler before the LED handles so any in-flight blink tasks
+    // are cancelled while the LEDs they reference are still alive.
+    task_scheduler_.reset();
+  }
+
+  nlohmann::json ToJson() const override;
+  nlohmann::json GetSchedulerStats() const override {
+    return task_scheduler_->ToJson();
+  }
+
+  // Returns the current behavior of a specific LED by name.
+  virtual absl::StatusOr<LedBehavior> GetLedBehavior(
+      absl::string_view led_name) const;
+
+  // Returns all LED names managed by this collector.
+  virtual std::vector<std::string> GetLedNames() const;
+
+  // Returns all preset names managed by this collector.
+  virtual std::vector<std::string> GetPresetNames() const;
+
+ protected:
+  LedCollector() = default;
+
+  // Sets the behavior of a specific LED by name.
+  virtual absl::Status SetLedBehavior(absl::string_view led_name,
+                                      const LedBehavior& behavior,
+                                      absl::Time timestamp);
+
+  // Applies a preset (sets the behavior for all member LEDs in the preset).
+  virtual absl::Status ApplyPreset(absl::string_view preset_name,
+                                   absl::Time timestamp);
+
+ private:
+  LedCollector(
+      std::unique_ptr<TaskScheduler> task_scheduler,
+      absl::flat_hash_map<std::string, std::unique_ptr<Led>> leds,
+      absl::flat_hash_map<std::string, std::vector<LedPresetBehavior>> presets)
+      : leds_(std::move(leds)),
+        presets_(std::move(presets)),
+        task_scheduler_(std::move(task_scheduler)) {}
+
+  const absl::flat_hash_map<std::string, std::unique_ptr<Led>> leds_;
+  const absl::flat_hash_map<std::string, std::vector<LedPresetBehavior>>
+      presets_;
+
+  // Reserved for future GPIO-backed LEDs that blink in software. Sysfs LEDs
+  // blink via the kernel timer trigger and do not use the scheduler.
+  // TaskScheduler must declared last so it is destroyed first.
+  std::unique_ptr<TaskScheduler> task_scheduler_;
+
+  friend class LedCollectorTest;
+};
+
+class EmptyLedCollector final : public LedCollector {
+ public:
+  static std::unique_ptr<EmptyLedCollector> Create();
+
+  nlohmann::json GetSchedulerStats() const override;
+
+  nlohmann::json ToJson() const override;
+};
+
+}  // namespace milotic_tlbmc
+
+#endif  // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_COLLECTOR_LED_COLLECTOR_H_
diff --git a/tlbmc/configs/proto_config_parser.cc b/tlbmc/configs/proto_config_parser.cc
index d1af44f..5516833 100644
--- a/tlbmc/configs/proto_config_parser.cc
+++ b/tlbmc/configs/proto_config_parser.cc
@@ -10,6 +10,7 @@
 #include "absl/strings/string_view.h"
 #include "gpio_config.pb.h"
 #include "gpio_fru_config.pb.h"
+#include "led_config.pb.h"
 #include "pcie_config.pb.h"
 #include "power_control.pb.h"
 #include "power_fault_log_config.pb.h"
@@ -72,6 +73,8 @@
   data_store_.usb_fru_component_configs =
       GetConfigFromProto<UsbFruComponentConfigs>(
           absl::StrCat(config_path_, "usb_fru_config.textproto"));
+  data_store_.led_configs = GetConfigFromProto<LedConfigs>(
+      absl::StrCat(config_path_, "led_config.textproto"));
 }
 
 SoftwareMetricsConfig ProtoConfigParser::GetSoftwareMetricsConfig() const {
@@ -110,6 +113,10 @@
   return data_store_.usb_fru_component_configs;
 }
 
+LedConfigs ProtoConfigParser::GetLedConfigs() const {
+  return data_store_.led_configs;
+}
+
 std::unique_ptr<ProtoConfigParser> ProtoConfigParser::Create(
     absl::string_view config_path) {
   return std::make_unique<ProtoConfigParser>(config_path);
diff --git a/tlbmc/configs/proto_config_parser.h b/tlbmc/configs/proto_config_parser.h
index d480fb7..1d61b6c 100644
--- a/tlbmc/configs/proto_config_parser.h
+++ b/tlbmc/configs/proto_config_parser.h
@@ -9,6 +9,7 @@
 #include "absl/strings/string_view.h"
 #include "gpio_config.pb.h"
 #include "gpio_fru_config.pb.h"
+#include "led_config.pb.h"
 #include "pcie_config.pb.h"
 #include "power_control.pb.h"
 #include "power_fault_log_config.pb.h"
@@ -29,6 +30,7 @@
   PcieConfigs pcie_configs;
   GpioFruComponentConfigs gpio_fru_component_configs;
   UsbFruComponentConfigs usb_fru_component_configs;
+  LedConfigs led_configs;
 };
 
 class ProtoConfigParser {
@@ -46,6 +48,7 @@
   PcieConfigs GetPcieConfigs() const;
   GpioFruComponentConfigs GetGpioFruComponentConfigs() const;
   UsbFruComponentConfigs GetUsbFruComponentConfigs() const;
+  LedConfigs GetLedConfigs() const;
 
   explicit ProtoConfigParser(absl::string_view config_path)
       : config_path_(config_path) {}
diff --git a/tlbmc/g3doc/modules/collectors.md b/tlbmc/g3doc/modules/collectors.md
index 1d3339d..7d470ef 100644
--- a/tlbmc/g3doc/modules/collectors.md
+++ b/tlbmc/g3doc/modules/collectors.md
@@ -93,6 +93,12 @@
 :             :                                    :         : aggregates    :
 :             :                                    :         : link          :
 :             :                                    :         : telemetry.    :
+| **LED       | `led_collector_module`             | `false` | Manages       |
+: Collector** :                                    :         : physical LEDs :
+:             :                                    :         : (sysfs/GPIO)  :
+:             :                                    :         : and applies   :
+:             :                                    :         : LED behavior  :
+:             :                                    :         : presets.      :
 
 --------------------------------------------------------------------------------
 
@@ -174,6 +180,14 @@
 -   Evaluates slot stable IDs against physical Redfish locations.
 -   Monitors presence via GPIO pins and aggregates link telemetry.
 
+### 10. [LED Collector](/third_party/milotic/external/cc/tlbmc/g3doc/components/collectors/led_collector.md) (`led_collector.cc`)
+
+Manages physical LEDs and applies preset behaviors:
+
+-   Controls hardware LED outputs (sysfs, GPIO) and states (ON, OFF, BLINK).
+-   Applies named LED presets (groupings of LED behaviors) across managed
+    indicators.
+
 ## Protobuf Configuration Schemas
 
 The collectors run background tasks that map hardware state to local memory
@@ -207,6 +221,10 @@
     [`thermal_config.proto`](http://google3/third_party/milotic/external/cc/tlbmc/configs/thermal_config.proto)):
     Defines multi-zone thermal PID coefficients and fan stepwise management
     rules.
+-   **`LedConfig`** / **`LedPreset`** (defined in
+    [`led_config.proto`](http://google3/third_party/milotic/external/cc/tlbmc/configs/led_config.proto)):
+    Defines LED hardware output interfaces (sysfs/GPIO), default behaviors, and
+    named preset mappings.
 
 --------------------------------------------------------------------------------
 
@@ -236,6 +254,9 @@
   sel_collector_module {
     enabled: true
   }
+  led_collector_module {
+    enabled: true
+  }
 }
 ```
 
@@ -255,6 +276,7 @@
 -   [`power_fault_log_entry.proto`](http://google3/third_party/milotic/external/cc/tlbmc/resource/power_fault_log_entry.proto)
 -   [`sse_event.proto`](http://google3/third_party/milotic/external/cc/tlbmc/resource/sse_event.proto)
 -   [`thermal_config.proto`](http://google3/third_party/milotic/external/cc/tlbmc/configs/thermal_config.proto)
+-   [`led_config.proto`](http://google3/third_party/milotic/external/cc/tlbmc/configs/led_config.proto)
 
 ### Core Code
 
@@ -268,3 +290,4 @@
 -   [`sel_collector.h`](http://google3/third_party/milotic/external/cc/tlbmc/collector/sel_collector.h)
 -   [`pcie_collector.h`](http://google3/third_party/milotic/external/cc/tlbmc/collector/pcie_collector.h)
 -   [`thermal_collector.h`](http://google3/third_party/milotic/external/cc/tlbmc/collector/thermal_collector.h)
+-   [`led_collector.h`](http://google3/third_party/milotic/external/cc/tlbmc/collector/led_collector.h)
diff --git a/tlbmc/hal/led/led.cc b/tlbmc/hal/led/led.cc
new file mode 100644
index 0000000..34e3af4
--- /dev/null
+++ b/tlbmc/hal/led/led.cc
@@ -0,0 +1,73 @@
+#include "tlbmc/hal/led/led.h"
+
+#include <optional>
+#include <string>
+
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/synchronization/mutex.h"
+#include "absl/time/time.h"
+#include "g3/macros.h"
+#include <nlohmann/json.hpp>
+#include "led_config.pb.h"
+#include "google/protobuf/json/json.h"
+#include "google/protobuf/util/json_util.h"
+
+namespace milotic_tlbmc {
+
+nlohmann::json Led::BehaviorToJson(const LedBehavior& behavior) {
+  std::string json_string;
+  ::google::protobuf::util::JsonPrintOptions opts;
+  opts.preserve_proto_field_names = true;
+  if (::google::protobuf::json::MessageToJsonString(behavior, &json_string, opts).ok()) {
+    return nlohmann::json::parse(json_string, nullptr, false);
+  }
+  LOG(ERROR) << "Failed to convert behavior to JSON: " << behavior;
+  return nullptr;
+}
+
+absl::Status Led::SetToDefaultBehavior(absl::Time timestamp) {
+  if (!default_behavior_.has_value() ||
+      default_behavior_->state() == LED_STATE_UNSPECIFIED) {
+    return absl::OkStatus();
+  }
+  return SetBehavior(*default_behavior_, timestamp);
+}
+
+absl::Status Led::SetBehavior(const LedBehavior& behavior,
+                              absl::Time timestamp) {
+  absl::MutexLock lock(mutex_);
+  if (last_update_time_.has_value() && timestamp <= *last_update_time_) {
+    return absl::OkStatus();
+  }
+  ECCLESIA_RETURN_IF_ERROR(SetBehaviorImpl(behavior));
+  last_update_time_ = timestamp;
+  return absl::OkStatus();
+}
+
+nlohmann::json Led::ToJson() const {
+  nlohmann::json led_json;
+  std::string name = GetName();
+  led_json["name"] = name;
+  if (default_behavior_.has_value()) {
+    led_json["default_behavior"] = BehaviorToJson(*default_behavior_);
+  }
+  {
+    absl::MutexLock lock(mutex_);
+    absl::StatusOr<LedBehavior> behavior = GetBehaviorImpl();
+    if (behavior.ok()) {
+      led_json["behavior"] = BehaviorToJson(*behavior);
+    } else {
+      LOG(ERROR) << "Failed to get behavior for LED " << name << " : "
+                 << behavior.status();
+      led_json["behavior"] = nullptr;
+    }
+    if (last_update_time_.has_value()) {
+      led_json["last_update_time"] = absl::FormatTime(*last_update_time_);
+    }
+  }
+  return led_json;
+}
+
+}  // namespace milotic_tlbmc
diff --git a/tlbmc/hal/led/led.h b/tlbmc/hal/led/led.h
new file mode 100644
index 0000000..63c55c6
--- /dev/null
+++ b/tlbmc/hal/led/led.h
@@ -0,0 +1,89 @@
+#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_H_
+#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_H_
+
+#include <optional>
+#include <string>
+
+#include "absl/base/thread_annotations.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/synchronization/mutex.h"
+#include "absl/time/time.h"
+#include <nlohmann/json.hpp>
+#include "led_config.pb.h"
+
+namespace milotic_tlbmc {
+// Interface for controlling a single LED.
+//
+// Implementations back the LED with a specific hardware interface (e.g. the
+// Linux LED sysfs subsystem, or - in the future - a raw GPIO line). Higher
+// level components such as the LedCollector interact with LEDs only through
+// this interface, so that support for new hardware backends can be added
+// without changing the collector.
+//
+// This class is thread-safe.
+class Led {
+ public:
+  static nlohmann::json BehaviorToJson(const LedBehavior& behavior);
+
+  virtual ~Led() = default;
+
+  // Led is a resource-managing class and should not be copied or moved.
+  Led(const Led&) = delete;
+  Led& operator=(const Led&) = delete;
+  Led(Led&&) = delete;
+  Led& operator=(Led&&) = delete;
+
+  // Returns the default behavior of the LED.
+  std::optional<LedBehavior> GetDefaultBehavior() const {
+    return default_behavior_;
+  }
+
+  // Returns the last update timestamp of the LED.
+  std::optional<absl::Time> GetLastUpdateTime() const {
+    absl::MutexLock lock(mutex_);
+    return last_update_time_;
+  }
+
+  // Sets the LED to its default behavior.
+  absl::Status SetToDefaultBehavior(absl::Time timestamp);
+
+  // Applies the given behavior (OFF / ON / BLINK) to the LED.
+  // If `timestamp` is less than or equal to `last_update_time_`, the update is
+  // skipped and absl::OkStatus() is returned.
+  absl::Status SetBehavior(const LedBehavior& behavior, absl::Time timestamp);
+
+  // Returns the current behavior of the LED, read back from the hardware.
+  absl::StatusOr<LedBehavior> GetBehavior() const {
+    absl::MutexLock lock(mutex_);
+    return GetBehaviorImpl();
+  }
+
+  // Returns the unique name of this LED (LedConfig.name).
+  std::string GetName() const { return name_; }
+
+  // Returns the JSON representation of the LED.
+  nlohmann::json ToJson() const;
+
+ protected:
+  explicit Led(const LedConfig& config)
+      : name_(config.name()),
+        default_behavior_(config.has_default_behavior()
+                              ? std::make_optional(config.default_behavior())
+                              : std::nullopt),
+        last_update_time_(std::nullopt) {}
+
+  // Hardware-specific implementation of setting the LED behavior.
+  virtual absl::Status SetBehaviorImpl(const LedBehavior& behavior) = 0;
+  // Hardware-specific implementation of reading the LED behavior.
+  virtual absl::StatusOr<LedBehavior> GetBehaviorImpl() const = 0;
+
+  const std::string name_;
+  const std::optional<LedBehavior> default_behavior_;
+  mutable absl::Mutex mutex_;
+  std::optional<absl::Time> last_update_time_ ABSL_GUARDED_BY(mutex_);
+};
+
+}  // namespace milotic_tlbmc
+
+#endif  // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_H_
diff --git a/tlbmc/hal/led/led_sysfs.cc b/tlbmc/hal/led/led_sysfs.cc
new file mode 100644
index 0000000..a11becf
--- /dev/null
+++ b/tlbmc/hal/led/led_sysfs.cc
@@ -0,0 +1,87 @@
+#include "tlbmc/hal/led/led_sysfs.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "g3/macros.h"
+#include "led_config.pb.h"
+#include "tlbmc/hal/sysfs/led.h"
+
+namespace milotic_tlbmc {
+
+absl::Status SysfsLed::SetBehaviorImpl(const LedBehavior& behavior) {
+  switch (behavior.state()) {
+    case LED_STATE_OFF:
+      ECCLESIA_RETURN_IF_ERROR(led_sysfs_.SetTrigger(sysfs_name_, "none"));
+      return led_sysfs_.SetBrightness(sysfs_name_, 0);
+    case LED_STATE_ON: {
+      ECCLESIA_RETURN_IF_ERROR(led_sysfs_.SetTrigger(sysfs_name_, "none"));
+      return led_sysfs_.SetBrightness(sysfs_name_, max_brightness_);
+    }
+    case LED_STATE_BLINK: {
+      // The kernel "timer" trigger blinks the LED in hardware using the
+      // delay_on / delay_off attributes.
+      ECCLESIA_RETURN_IF_ERROR(led_sysfs_.SetTrigger(sysfs_name_, "timer"));
+      ECCLESIA_RETURN_IF_ERROR(led_sysfs_.SetDelayOn(
+          sysfs_name_, behavior.blink_config().duty_on_ms()));
+      return led_sysfs_.SetDelayOff(sysfs_name_,
+                                    behavior.blink_config().duty_off_ms());
+    }
+    default:
+      return absl::InvalidArgumentError("Unknown LED state");
+  }
+}
+
+absl::StatusOr<LedBehavior> SysfsLed::GetBehaviorImpl() const {
+  ECCLESIA_ASSIGN_OR_RETURN(std::string trigger,
+                            led_sysfs_.GetTrigger(sysfs_name_));
+  if (trigger == "timer") {
+    ECCLESIA_ASSIGN_OR_RETURN(uint32_t delay_on,
+                              led_sysfs_.GetDelayOn(sysfs_name_));
+    ECCLESIA_ASSIGN_OR_RETURN(uint32_t delay_off,
+                              led_sysfs_.GetDelayOff(sysfs_name_));
+    LedBehavior behavior;
+    behavior.set_state(LED_STATE_BLINK);
+    behavior.mutable_blink_config()->set_duty_on_ms(delay_on);
+    behavior.mutable_blink_config()->set_duty_off_ms(delay_off);
+    return behavior;
+  }
+
+  ECCLESIA_ASSIGN_OR_RETURN(uint32_t brightness,
+                            led_sysfs_.GetBrightness(sysfs_name_));
+  LedBehavior behavior;
+  behavior.set_state(brightness > 0 ? LED_STATE_ON : LED_STATE_OFF);
+  return behavior;
+}
+
+absl::StatusOr<std::unique_ptr<SysfsLed>> SysfsLed::Create(
+    const LedConfig& config, const LedSysfs& led_sysfs) {
+  if (config.name().empty()) {
+    return absl::InvalidArgumentError("LedConfig name must not be empty");
+  }
+  if (!config.has_output()) {
+    return absl::InvalidArgumentError("LedConfig output must be set");
+  }
+  if (!config.output().has_sysfs_name() ||
+      config.output().sysfs_name().empty()) {
+    return absl::InvalidArgumentError("Sysfs LED name must not be empty");
+  }
+
+  if (!led_sysfs.IsLedPresent(config.output().sysfs_name())) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Sysfs LED ", config.output().sysfs_name(), " not found"));
+  }
+
+  ECCLESIA_ASSIGN_OR_RETURN(
+      uint32_t max_brightness,
+      led_sysfs.GetMaxBrightness(config.output().sysfs_name()));
+
+  return absl::WrapUnique(new SysfsLed(config, led_sysfs, max_brightness));
+}
+
+}  // namespace milotic_tlbmc
diff --git a/tlbmc/hal/led/led_sysfs.h b/tlbmc/hal/led/led_sysfs.h
new file mode 100644
index 0000000..d7b945c
--- /dev/null
+++ b/tlbmc/hal/led/led_sysfs.h
@@ -0,0 +1,50 @@
+#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_SYSFS_H_
+#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_SYSFS_H_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "led_config.pb.h"
+#include "tlbmc/hal/led/led.h"
+#include "tlbmc/hal/sysfs/led.h"
+
+namespace milotic_tlbmc {
+// LED backed by the Linux LED sysfs subsystem.
+//
+// Reuses the LedSysfs HAL to translate high-level LedBehavior into sysfs
+// attribute writes:
+//   - LED_STATE_OFF:   trigger="none", brightness=0.
+//   - LED_STATE_ON:    trigger="none", brightness=max_brightness.
+//   - LED_STATE_BLINK: trigger="timer", delay_on/delay_off derived from the
+//                      blink period and duty cycle. Blinking is handled by the
+//                      kernel timer trigger, so no software timer is required.
+//
+// This class is thread-safe.
+class SysfsLed : public Led {
+ public:
+  static absl::StatusOr<std::unique_ptr<SysfsLed>> Create(
+      const LedConfig& config, const LedSysfs& led_sysfs);
+
+ protected:
+  absl::Status SetBehaviorImpl(const LedBehavior& behavior) override;
+  absl::StatusOr<LedBehavior> GetBehaviorImpl() const override;
+
+  SysfsLed(const LedConfig& config, const LedSysfs& led_sysfs,
+           uint32_t max_brightness = 255)
+      : Led(config),
+        led_sysfs_(led_sysfs),
+        sysfs_name_(config.output().sysfs_name()),
+        max_brightness_(max_brightness) {}
+
+ private:
+  const LedSysfs& led_sysfs_;
+  const std::string sysfs_name_;
+  const uint32_t max_brightness_;
+};
+
+}  // namespace milotic_tlbmc
+
+#endif  // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_LED_LED_SYSFS_H_
diff --git a/tlbmc/hal/sysfs/led.cc b/tlbmc/hal/sysfs/led.cc
new file mode 100644
index 0000000..eb7ae27
--- /dev/null
+++ b/tlbmc/hal/sysfs/led.cc
@@ -0,0 +1,202 @@
+#include "tlbmc/hal/sysfs/led.h"
+
+#include <cstdint>
+#include <fstream>
+#include <string>
+#include <vector>
+
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
+#include "absl/strings/string_view.h"
+#include "absl/strings/strip.h"
+#include "boost/filesystem.hpp"  // NOLINT: boost::filesystem is commonly used in BMC
+
+namespace milotic_tlbmc {
+namespace {
+
+std::string GetLedAttributePath(absl::string_view sysfs_path,
+                                absl::string_view led_name,
+                                absl::string_view attribute) {
+  return absl::StrCat(sysfs_path, led_name, "/", attribute);
+}
+
+absl::StatusOr<uint32_t> ReadUint32FromFile(const std::string& path) {
+  std::ifstream file(path);
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to open ", path));
+  }
+
+  std::string content;
+  std::getline(file, content);
+  if (!file.good() && !file.eof()) {
+    return absl::InternalError(absl::StrCat("Failed to read from ", path));
+  }
+
+  uint32_t value;
+  if (!absl::SimpleAtoi(content, &value)) {
+    return absl::InternalError(
+        absl::StrCat("Failed to parse integer from '", content, "' in ", path));
+  }
+
+  return value;
+}
+
+absl::Status WriteUint32ToFile(const std::string& path, uint32_t value) {
+  std::ofstream file(path);
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to open ", path));
+  }
+
+  file << value << "\n";
+  file.flush();
+
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to write to ", path));
+  }
+
+  return absl::OkStatus();
+}
+
+absl::Status WriteStringToFile(const std::string& path,
+                               absl::string_view value) {
+  std::ofstream file(path);
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to open ", path));
+  }
+
+  file << value << "\n";
+  file.flush();
+
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to write to ", path));
+  }
+
+  return absl::OkStatus();
+}
+
+}  // namespace
+
+bool LedSysfs::IsLedPresent(absl::string_view led_name) const {
+  boost::filesystem::path path(absl::StrCat(config_.led_sysfs_path, led_name));
+  boost::system::error_code ec;
+  bool exists = boost::filesystem::exists(path, ec);
+  if (ec) {
+    LOG(ERROR) << "Error checking exists for path '" << path
+               << "': " << ec.message();
+    return false;
+  }
+  return exists;
+}
+
+absl::StatusOr<uint32_t> LedSysfs::GetBrightness(
+    absl::string_view led_name) const {
+  return ReadUint32FromFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "brightness"));
+}
+
+absl::Status LedSysfs::SetBrightness(absl::string_view led_name,
+                                     uint32_t brightness) const {
+  return WriteUint32ToFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "brightness"),
+      brightness);
+}
+
+absl::StatusOr<uint32_t> LedSysfs::GetMaxBrightness(
+    absl::string_view led_name) const {
+  return ReadUint32FromFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "max_brightness"));
+}
+
+absl::StatusOr<std::string> LedSysfs::GetTrigger(
+    absl::string_view led_name) const {
+  std::string path =
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "trigger");
+  std::ifstream file(path);
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to open ", path));
+  }
+
+  std::string content;
+  std::getline(file, content);
+  if (!file.good() && !file.eof()) {
+    return absl::InternalError(absl::StrCat("Failed to read from ", path));
+  }
+
+  // The active trigger is enclosed in brackets: "none [timer] heartbeat"
+  // Find the bracketed trigger.
+  auto start = content.find('[');
+  auto end = content.find(']');
+  if (start == std::string::npos || end == std::string::npos || end <= start) {
+    return absl::InternalError(absl::StrCat(
+        "Failed to parse active trigger from '", content, "' in ", path));
+  }
+
+  return content.substr(start + 1, end - start - 1);
+}
+
+absl::StatusOr<std::vector<std::string>> LedSysfs::GetAvailableTriggers(
+    absl::string_view led_name) const {
+  std::string path =
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "trigger");
+  std::ifstream file(path);
+  if (!file.good()) {
+    return absl::InternalError(absl::StrCat("Failed to open ", path));
+  }
+
+  std::string content;
+  std::getline(file, content);
+  if (!file.good() && !file.eof()) {
+    return absl::InternalError(absl::StrCat("Failed to read from ", path));
+  }
+
+  // Split by whitespace, stripping brackets from the active trigger.
+  std::vector<std::string> triggers;
+  for (absl::string_view token :
+       absl::StrSplit(content, ' ', absl::SkipEmpty())) {
+    absl::string_view stripped = token;
+    absl::ConsumePrefix(&stripped, "[");
+    absl::ConsumeSuffix(&stripped, "]");
+    triggers.emplace_back(stripped);
+  }
+
+  return triggers;
+}
+
+absl::Status LedSysfs::SetTrigger(absl::string_view led_name,
+                                  absl::string_view trigger) const {
+  return WriteStringToFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "trigger"),
+      trigger);
+}
+
+absl::StatusOr<uint32_t> LedSysfs::GetDelayOn(
+    absl::string_view led_name) const {
+  return ReadUint32FromFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "delay_on"));
+}
+
+absl::Status LedSysfs::SetDelayOn(absl::string_view led_name,
+                                  uint32_t delay_ms) const {
+  return WriteUint32ToFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "delay_on"),
+      delay_ms);
+}
+
+absl::StatusOr<uint32_t> LedSysfs::GetDelayOff(
+    absl::string_view led_name) const {
+  return ReadUint32FromFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "delay_off"));
+}
+
+absl::Status LedSysfs::SetDelayOff(absl::string_view led_name,
+                                   uint32_t delay_ms) const {
+  return WriteUint32ToFile(
+      GetLedAttributePath(config_.led_sysfs_path, led_name, "delay_off"),
+      delay_ms);
+}
+
+}  // namespace milotic_tlbmc
diff --git a/tlbmc/hal/sysfs/led.h b/tlbmc/hal/sysfs/led.h
new file mode 100644
index 0000000..d7f4405
--- /dev/null
+++ b/tlbmc/hal/sysfs/led.h
@@ -0,0 +1,95 @@
+#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_SYSFS_LED_H_
+#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_SYSFS_LED_H_
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+
+namespace milotic_tlbmc {
+
+struct LedSysfsConfig {
+  std::string led_sysfs_path = "/sys/class/leds/";
+};
+
+// An abstraction for interacting with the Linux LED sysfs subsystem.
+//
+// The Linux LED subsystem exposes LEDs at /sys/class/leds/<led_name>/ with
+// the following key attributes:
+//   - brightness:     Current brightness level (0 to max_brightness).
+//   - max_brightness: Maximum brightness value supported by the LED.
+//   - trigger:        The trigger source that controls the LED behavior
+//                     (e.g., "none", "timer", "heartbeat", "default-on").
+//   - delay_on:       Duration in milliseconds the LED stays on during
+//                     blinking (only available when trigger is "timer").
+//   - delay_off:      Duration in milliseconds the LED stays off during
+//                     blinking (only available when trigger is "timer").
+//
+// The class is thread-safe. Thread safety is mostly guaranteed by the
+// underlying sysfs filesystem.
+class LedSysfs {
+ public:
+  // Default constructor for unit tests.
+  LedSysfs() : LedSysfs(LedSysfsConfig()) {}
+
+  explicit LedSysfs(const LedSysfsConfig& config) : config_(config) {}
+
+  virtual ~LedSysfs() = default;
+
+  // Returns the current brightness of the LED.
+  virtual absl::StatusOr<uint32_t> GetBrightness(
+      absl::string_view led_name) const;
+
+  // Sets the brightness of the LED to the given value.
+  // The value must be in the range [0, max_brightness].
+  virtual absl::Status SetBrightness(absl::string_view led_name,
+                                     uint32_t brightness) const;
+
+  // Returns the maximum brightness value supported by the LED.
+  virtual absl::StatusOr<uint32_t> GetMaxBrightness(
+      absl::string_view led_name) const;
+
+  // Returns the current trigger of the LED.
+  // The active trigger is enclosed in brackets in the sysfs trigger file,
+  // e.g., "none [timer] heartbeat default-on".
+  virtual absl::StatusOr<std::string> GetTrigger(
+      absl::string_view led_name) const;
+
+  // Returns the list of available triggers for the LED.
+  virtual absl::StatusOr<std::vector<std::string>> GetAvailableTriggers(
+      absl::string_view led_name) const;
+
+  // Sets the trigger of the LED to the given value.
+  virtual absl::Status SetTrigger(absl::string_view led_name,
+                                  absl::string_view trigger) const;
+
+  // Returns the delay_on value in milliseconds (timer trigger only).
+  virtual absl::StatusOr<uint32_t> GetDelayOn(absl::string_view led_name) const;
+
+  // Sets the delay_on value in milliseconds (timer trigger only).
+  virtual absl::Status SetDelayOn(absl::string_view led_name,
+                                  uint32_t delay_ms) const;
+
+  // Returns the delay_off value in milliseconds (timer trigger only).
+  virtual absl::StatusOr<uint32_t> GetDelayOff(
+      absl::string_view led_name) const;
+
+  // Sets the delay_off value in milliseconds (timer trigger only).
+  virtual absl::Status SetDelayOff(absl::string_view led_name,
+                                   uint32_t delay_ms) const;
+
+  // Returns true if the LED exists in sysfs.
+  virtual bool IsLedPresent(absl::string_view led_name) const;
+
+  std::string GetLedSysfsPath() const { return config_.led_sysfs_path; }
+
+ private:
+  const LedSysfsConfig config_;
+};
+
+}  // namespace milotic_tlbmc
+
+#endif  // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_HAL_SYSFS_LED_H_
diff --git a/tlbmc/led_config.proto b/tlbmc/led_config.proto
new file mode 100644
index 0000000..c80dc60
--- /dev/null
+++ b/tlbmc/led_config.proto
@@ -0,0 +1,89 @@
+edition = "2023";
+
+package milotic_tlbmc;
+
+import "power_control.proto";
+
+enum LedState {
+  LED_STATE_UNSPECIFIED = 0;
+  LED_STATE_OFF = 1;
+  LED_STATE_BLINK = 2;
+  LED_STATE_ON = 3;
+}
+
+message BlinkConfig {
+  uint32 duty_on_ms = 1 [default = 500];
+  uint32 duty_off_ms = 2 [default = 500];
+}
+
+message LedBehavior {
+  LedState state = 1 [default = LED_STATE_OFF];
+  // Configuration applied ONLY if state == LED_STATE_BLINK.
+  BlinkConfig blink_config = 2;
+}
+
+message LedOutput {
+  oneof target {
+    // Name of the sysfs LED (e.g., "blue:identify").
+    string sysfs_name = 1;
+    // Name of the GPIO LED line (e.g., "power_led" or "<chip_id>:<gpio_num>").
+    // TODO: b/541951164 - impl GPIO LEDs.
+    // string gpio_name = 2;
+  }
+}
+
+message LedConfig {
+  // Unique name identifying the LED (e.g., "fault_led", "identify_led").
+  string name = 1;
+  // Hardware output interface (sysfs or GPIO).
+  LedOutput output = 2;
+  // Default behavior applied on start / no rules matched
+  // set to LED_STATE_UNSPECIFIED to persist the previous state
+  LedBehavior default_behavior = 3;
+}
+
+message LedPresetMember {
+  string name = 1;
+  LedBehavior behavior = 2;
+}
+
+message LedPreset {
+  // Name of the preset (e.g., "system_fault", "chassis_identify", "power_on").
+  string name = 1;
+  // List of LEDs and their associated state controls.
+  repeated LedPresetMember members = 2;
+}
+
+message LedConditionVariable {
+  string name = 1;
+  oneof value {
+    string source_path = 2;
+    PowerState power_state = 3;
+    OSState os_state = 4;
+  }
+}
+
+message LedCondition {
+  string expression = 1;
+}
+
+enum LedTrigger {
+  LED_TRIGGER_UNSPECIFIED = 0;
+  // TODO: b/546330351 - impl GPIO trigger
+}
+
+message LedPolicy {
+  string name = 1;
+  string preset_name = 2;
+  oneof event {
+    LedCondition condition = 3;
+    LedTrigger trigger = 4;
+  }
+}
+
+message LedConfigs {
+  repeated LedConfig leds = 1;
+  repeated LedPreset presets = 2;
+  repeated LedConditionVariable variables = 3;
+  repeated LedPolicy policies = 4;
+}
diff --git a/tlbmc/meson.build b/tlbmc/meson.build
index 3f40afa..deaa482 100644
--- a/tlbmc/meson.build
+++ b/tlbmc/meson.build
@@ -28,6 +28,7 @@
   'hwmon_temp_sensor_config.proto',
   'identifier.proto',
   'intel_cpu_sensor_config.proto',
+  'led_config.proto',
   'memory.proto',
   'nic_telemetry_config.proto',
   'offline_data.proto',
@@ -138,6 +139,7 @@
   'collector/fake_gpio_collector.cc',
   'collector/fru_collector.cc',
   'collector/gpio_collector.cc',
+  'collector/led_collector.cc',
   'collector/metric_collector.cc',
   'collector/pcie_collector.cc',
   'collector/peci_scanner.cc',
@@ -173,6 +175,8 @@
   'hal/gpio/gpio.cc',
   'hal/gpio/gpio_monitor.cc',
   'hal/gpio/on_demand_gpio.cc',
+  'hal/led/led.cc',
+  'hal/led/led_sysfs.cc',
   'hal/nic_veeprom/interface.cc',
   'hal/peci/peci_access_impl.cc',
   'hal/power_fault_detector/power_fault_detector.cc',
@@ -184,6 +188,7 @@
   'hal/sysfs/i2c.cc',
   'hal/sysfs/i3c.cc',
   'hal/sysfs/iio.cc',
+  'hal/sysfs/led.cc',
   'hal/sysfs/peci.cc',
   'hal/system_registry_impl.cc',
   'hft/core/edge_filter.cc',
diff --git a/tlbmc/store/factory.h b/tlbmc/store/factory.h
index dd739d8..1da3848 100644
--- a/tlbmc/store/factory.h
+++ b/tlbmc/store/factory.h
@@ -13,6 +13,7 @@
 #include <nlohmann/json_fwd.hpp>
 #include "tlbmc/collector/fru_collector.h"
 #include "tlbmc/collector/gpio_collector.h"
+#include "tlbmc/collector/led_collector.h"
 #include "tlbmc/collector/metric_collector.h"
 #include "tlbmc/collector/pcie_collector.h"
 #include "tlbmc/collector/power_control_collector.h"
@@ -43,6 +44,8 @@
   CreateMetricCollector(const MetricCollector::Params& params) = 0;
   virtual absl::StatusOr<std::unique_ptr<GpioCollector>> CreateGpioCollector(
       const GpioCollector::Params& params) = 0;
+  virtual absl::StatusOr<std::unique_ptr<LedCollector>> CreateLedCollector(
+      const LedCollector::Params& params) = 0;
   virtual absl::StatusOr<std::unique_ptr<ThermalCollector>>
   CreateThermalCollector(const ThermalCollector::Params& params,
                          SensorCollectorAggregator* absl_nonnull aggregator,
@@ -81,6 +84,11 @@
     return GpioCollector::Create(params);
   }
 
+  absl::StatusOr<std::unique_ptr<LedCollector>> CreateLedCollector(
+      const LedCollector::Params& params) override {
+    return LedCollector::Create(params);
+  }
+
   absl::StatusOr<std::unique_ptr<ThermalCollector>> CreateThermalCollector(
       const ThermalCollector::Params& params,
       SensorCollectorAggregator* absl_nonnull aggregator,
diff --git a/tlbmc/store/octeon10_evk_store.cc b/tlbmc/store/octeon10_evk_store.cc
index caec0c6..7c35b5f 100644
--- a/tlbmc/store/octeon10_evk_store.cc
+++ b/tlbmc/store/octeon10_evk_store.cc
@@ -29,6 +29,7 @@
 #include "tlbmc/collector/collector.h"
 #include "tlbmc/collector/fru_collector.h"
 #include "tlbmc/collector/gpio_collector.h"
+#include "tlbmc/collector/led_collector.h"
 #include "tlbmc/collector/monitoring_change_base.h"
 #include "tlbmc/collector/pcie_collector.h"
 #include "power_control.pb.h"
@@ -691,6 +692,8 @@
 
 GpioCollector* Octeon10EvkStore::GetGpioCollector() const { return nullptr; }
 
+LedCollector* Octeon10EvkStore::GetLedCollector() const { return nullptr; }
+
 PcieCollector* Octeon10EvkStore::GetPcieCollector() const { return nullptr; }
 
 SoftwareMetricsValue Octeon10EvkStore::GetMetricValues() const {
diff --git a/tlbmc/store/octeon10_evk_store.h b/tlbmc/store/octeon10_evk_store.h
index c29df30..9510684 100644
--- a/tlbmc/store/octeon10_evk_store.h
+++ b/tlbmc/store/octeon10_evk_store.h
@@ -118,6 +118,7 @@
 
   FruCollector* GetFruCollector() const override;
   GpioCollector* GetGpioCollector() const override;
+  LedCollector* GetLedCollector() const override;
   PcieCollector* GetPcieCollector() const override;
 
   SoftwareMetricsValue GetMetricValues() const override;
diff --git a/tlbmc/store/store.h b/tlbmc/store/store.h
index 64c27e1..56a430d 100644
--- a/tlbmc/store/store.h
+++ b/tlbmc/store/store.h
@@ -12,6 +12,7 @@
 
 #include "absl/time/time.h"
 #include "tlbmc/collector/gpio_collector.h"
+#include "tlbmc/collector/led_collector.h"
 #include "tlbmc/collector/sensor_collector.h"
 #include "resource.pb.h"
 #include "tlbmc/thermal/controller/dff_controller.h"
@@ -171,6 +172,7 @@
 
   virtual FruCollector* GetFruCollector() const = 0;
   virtual GpioCollector* GetGpioCollector() const = 0;
+  virtual LedCollector* GetLedCollector() const = 0;
   virtual PcieCollector* GetPcieCollector() const = 0;
 
   virtual SoftwareMetricsValue GetMetricValues() const = 0;
@@ -470,6 +472,7 @@
 
   FruCollector* GetFruCollector() const override { return nullptr; }
   GpioCollector* GetGpioCollector() const override { return nullptr; }
+  LedCollector* GetLedCollector() const override { return nullptr; }
   PcieCollector* GetPcieCollector() const override { return nullptr; }
 
   SoftwareMetricsValue GetMetricValues() const override {
diff --git a/tlbmc/store/store_impl.cc b/tlbmc/store/store_impl.cc
index c09acb5..78434e5 100644
--- a/tlbmc/store/store_impl.cc
+++ b/tlbmc/store/store_impl.cc
@@ -35,6 +35,7 @@
 #include "tlbmc/collector/collector.h"
 #include "tlbmc/collector/fru_collector.h"
 #include "tlbmc/collector/gpio_collector.h"
+#include "tlbmc/collector/led_collector.h"
 #include "tlbmc/collector/metric_collector.h"
 #include "tlbmc/collector/monitoring_change_base.h"
 #include "tlbmc/collector/pcie_collector.h"
@@ -108,6 +109,10 @@
   return all_collectors_.gpio_collector.get();
 }
 
+LedCollector* StoreImpl::GetLedCollector() const {
+  return all_collectors_.led_collector.get();
+}
+
 PcieCollector* StoreImpl::GetPcieCollector() const {
   return all_collectors_.pcie_collector.get();
 }
@@ -440,6 +445,7 @@
   json["Fru"] = all_collectors_.fru_collector->ToJson();
   json["Metric"] = all_collectors_.metric_collector->ToJson();
   json["Gpio"] = all_collectors_.gpio_collector->ToJson();
+  json["Led"] = all_collectors_.led_collector->ToJson();
   json["Fan"] = all_collectors_.thermal_collector->ToJson();
   json["PowerFaultLog"] = all_collectors_.power_fault_log_collector->ToJson();
   json["Sel"] = all_collectors_.sel_collector->ToJson();
@@ -457,6 +463,7 @@
   json["MetricCollector"] =
       all_collectors_.metric_collector->GetSchedulerStats();
   json["GpioCollector"] = all_collectors_.gpio_collector->GetSchedulerStats();
+  json["LedCollector"] = all_collectors_.led_collector->GetSchedulerStats();
   json["Fan"] = all_collectors_.thermal_collector->GetSchedulerStats();
   json["PowerFaultLogCollector"] =
       all_collectors_.power_fault_log_collector->GetSchedulerStats();
@@ -786,6 +793,7 @@
       .metric_collector = EmptyMetricCollector::Create(),
       .gpio_collector = EmptyGpioCollector::Create(),
       .power_control_collector = EmptyPowerControlCollector::Create(),
+      .led_collector = EmptyLedCollector::Create(),
       .sensor_collector = EmptySensorCollector::Create(),
       .thermal_collector = EmptyThermalCollector::Create(),
       .power_fault_log_collector = EmptyPowerFaultLogCollector::Create(),
@@ -822,6 +830,15 @@
     all_collectors.power_control_collector = *std::move(collector);
   }
 
+  if (GetTlbmcConfig().led_collector_module().enabled()) {
+    ECCLESIA_ASSIGN_OR_RETURN(
+        std::unique_ptr<LedCollector> led_collector,
+        options.collector_factory->CreateLedCollector(
+            {.led_configs = options.proto_parser->GetLedConfigs(),
+             .led_sysfs = *options.led_sysfs}));
+    all_collectors.led_collector = std::move(led_collector);
+  }
+
   if (GetTlbmcConfig().fru_collector_module().enabled()) {
     absl::Time fru_scan_start_time = absl::Now();
     Tracer::GetInstance().AddOneOffDatapoint("Tlbmc-Scan-FRUs-Begin",
diff --git a/tlbmc/store/store_impl.h b/tlbmc/store/store_impl.h
index 0f2b272..37e56a8 100644
--- a/tlbmc/store/store_impl.h
+++ b/tlbmc/store/store_impl.h
@@ -26,6 +26,7 @@
 #include "tlbmc/collector/collector.h"
 #include "tlbmc/collector/fru_collector.h"
 #include "tlbmc/collector/gpio_collector.h"
+#include "tlbmc/collector/led_collector.h"
 #include "tlbmc/collector/metric_collector.h"
 #include "tlbmc/collector/monitoring_change_base.h"
 #include "tlbmc/collector/pcie_collector.h"
@@ -49,6 +50,7 @@
 #include "tlbmc/hal/sysfs/i2c.h"
 #include "tlbmc/hal/sysfs/i3c.h"
 #include "tlbmc/hal/sysfs/iio.h"
+#include "tlbmc/hal/sysfs/led.h"
 #include "tlbmc/hal/sysfs/peci.h"
 #include "tlbmc/hal/system_registry.h"
 #include "tlbmc/host_state/power_control.h"
@@ -81,6 +83,7 @@
   std::unique_ptr<MetricCollector> metric_collector = nullptr;
   std::unique_ptr<GpioCollector> gpio_collector = nullptr;
   std::unique_ptr<PowerControlCollector> power_control_collector = nullptr;
+  std::unique_ptr<LedCollector> led_collector = nullptr;
   std::unique_ptr<SensorCollector> sensor_collector = nullptr;
   std::unique_ptr<ThermalCollector> thermal_collector = nullptr;
   std::unique_ptr<PowerFaultLogCollector> power_fault_log_collector = nullptr;
@@ -106,6 +109,7 @@
     std::unique_ptr<IioSysfs> iio_sysfs = nullptr;
     std::unique_ptr<I2cSysfs> i2c_sysfs = nullptr;
     std::unique_ptr<I3cSysfs> i3c_sysfs = nullptr;
+    std::unique_ptr<LedSysfs> led_sysfs = nullptr;
     std::unique_ptr<PeciSysfs> peci_sysfs = nullptr;
     std::unique_ptr<PeciAccessInterface> peci_access =
         std::make_unique<PeciAccessImpl>();
@@ -186,6 +190,7 @@
     bool tlbmc_unit_test_enable_fake_gpio_collector = false;
 
     std::unique_ptr<GpioCollector> gpio_collector = nullptr;
+    std::unique_ptr<LedCollector> led_collector = nullptr;
 
     absl::AnyInvocable<std::unique_ptr<nic_veeprom::Accessor>(
         nic_veeprom::NicTelemetryVersion version, int bus, int address) const>
@@ -257,6 +262,7 @@
   // Returns the fru collector as raw pointer.
   FruCollector* GetFruCollector() const override;
   GpioCollector* GetGpioCollector() const override;
+  LedCollector* GetLedCollector() const override;
   PcieCollector* GetPcieCollector() const override;
 
   absl::StatusOr<PsiMetrics> GetPsiMetrics() const override;