From 4500f41983ad62f0643b3103e1c768befddd31fc Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Sat, 29 Nov 2025 13:12:18 +0100 Subject: [PATCH] feat: add ugreen nas leds --- hosts/nas/configuration.nix | 1 + hosts/nas/modules/ugreen-leds.nix | 285 ++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 hosts/nas/modules/ugreen-leds.nix diff --git a/hosts/nas/configuration.nix b/hosts/nas/configuration.nix index 7cddbcb..4091bd5 100644 --- a/hosts/nas/configuration.nix +++ b/hosts/nas/configuration.nix @@ -15,6 +15,7 @@ in { ./modules/jellyfin.nix ./modules/power-management.nix ./modules/disk-monitoring.nix + ./modules/ugreen-leds.nix ./hardware-configuration.nix ]; diff --git a/hosts/nas/modules/ugreen-leds.nix b/hosts/nas/modules/ugreen-leds.nix new file mode 100644 index 0000000..b859427 --- /dev/null +++ b/hosts/nas/modules/ugreen-leds.nix @@ -0,0 +1,285 @@ +# UGREEN DXP4800 LED control +# Based on https://github.com/miskcoo/ugreen_leds_controller +{ config, lib, pkgs, ... }: + +let + # Disk mapping: ata port -> LED name + # DXP4800 has bays 1-4, currently bays 2 and 4 are populated + diskMapping = { + # ata-2 (sdb) -> disk2 + "2" = "disk2"; + # ata-4 (sdc) -> disk4 + "4" = "disk4"; + }; + + # LED colors (R G B) + colors = { + healthy = "0 255 0"; # Green + activity = "0 255 0"; # Green blink + standby = "0 0 255"; # Dim blue when sleeping + fail = "255 0 0"; # Red + network = "0 255 0"; # Green + power = "255 255 255"; # White + }; + + brightness = 255; + standbyBrightness = 30; # Dim when in standby + refreshInterval = "0.1"; # Seconds between activity checks + powerCheckInterval = 30; # Seconds between power state checks + + # Script to initialize LEDs on boot + initLedsScript = pkgs.writeShellScript "ugreen-leds-init" '' + set -euo pipefail + PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli ]}:$PATH" + + # Wait for i2c device to be available + for i in $(seq 1 30); do + if [ -e /dev/i2c-0 ]; then + break + fi + sleep 1 + done + + # Initialize power LED - solid white + ugreen_leds_cli power -on -color ${colors.power} -brightness ${toString brightness} || true + + # Initialize network LED - will be controlled by netdevmon + ugreen_leds_cli netdev -off || true + + # Initialize disk LEDs based on mapping + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (ata: led: '' + ugreen_leds_cli ${led} -off || true + '') diskMapping)} + + echo "UGREEN LEDs initialized" + ''; + + # Disk activity monitoring script + diskMonitorScript = pkgs.writeShellScript "ugreen-diskiomon" '' + set -euo pipefail + PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli pkgs.coreutils pkgs.gnugrep pkgs.gawk pkgs.smartmontools pkgs.hdparm ]}:$PATH" + + # Build device -> LED mapping by checking ata ports + declare -A devices + declare -A diskio_data + declare -A disk_healthy + declare -A disk_standby + + # Discover disks based on ata port mapping + for path in /dev/disk/by-path/pci-*-ata-*; do + [ -e "$path" ] || continue + # Skip partitions + [[ "$path" == *-part* ]] && continue + + # Extract ata port number (e.g., ata-2 -> 2) + ata_port=$(echo "$path" | grep -oP 'ata-\K[0-9]+' | head -1) + + case "$ata_port" in + ${lib.concatStringsSep "\n " (lib.mapAttrsToList (ata: led: '' + ${ata}) + device=$(readlink -f "$path") + short_name=$(basename "$device") + devices["${led}"]="$short_name" + echo "Mapped $short_name (ata-${ata}) -> ${led}" + ;;'') diskMapping)} + esac + done + + if [ ''${#devices[@]} -eq 0 ]; then + echo "No disks found matching ATA ports, exiting" + exit 1 + fi + + # Set initial LED state for discovered disks + for led in "''${!devices[@]}"; do + device="''${devices[$led]}" + + # Check SMART health (this will wake the disk at boot, which is acceptable) + if smartctl -H "/dev/$device" 2>/dev/null | grep -q "PASSED"; then + disk_healthy["$led"]=1 + ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true + else + disk_healthy["$led"]=0 + ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true + fi + + # Initialize tracking + diskio_data["$led"]="" + disk_standby["$led"]=0 + done + + echo "Starting disk activity monitoring for ''${#devices[@]} disk(s)" + + # Function to update LED based on current state + update_led() { + local led="$1" + local device="''${devices[$led]}" + + # Check power state without waking disk + local power_state + power_state=$(hdparm -C "/dev/$device" 2>/dev/null | grep -oP '(standby|active/idle|active|idle)' | head -1 || echo "unknown") + + if [[ "$power_state" == "standby" ]]; then + if [[ "''${disk_standby[$led]}" != "1" ]]; then + # Disk just went to standby - dim the LED + disk_standby["$led"]=1 + ugreen_leds_cli "$led" -on -color ${colors.standby} -brightness ${toString standbyBrightness} || true + echo "Disk $device entered standby, dimming LED" + fi + else + if [[ "''${disk_standby[$led]}" == "1" ]]; then + # Disk woke up - restore health-based color + disk_standby["$led"]=0 + if [[ "''${disk_healthy[$led]}" == "1" ]]; then + ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true + else + ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true + fi + echo "Disk $device woke up, restoring LED" + fi + fi + } + + # Background power state checker + check_power_states() { + while true; do + sleep ${toString powerCheckInterval} + for led in "''${!devices[@]}"; do + update_led "$led" + done + done + } + + # Start power state checker in background + check_power_states & + POWER_CHECK_PID=$! + trap "kill $POWER_CHECK_PID 2>/dev/null || true" EXIT + + # Main activity monitoring loop + while true; do + for led in "''${!devices[@]}"; do + device="''${devices[$led]}" + stat_file="/sys/block/$device/stat" + + if [ -f "$stat_file" ]; then + new_stat=$(cat "$stat_file" 2>/dev/null || echo "") + + if [ -n "$new_stat" ] && [ "''${diskio_data[$led]}" != "$new_stat" ]; then + # Activity detected - disk must be awake now + if [[ "''${disk_standby[$led]}" == "1" ]]; then + disk_standby["$led"]=0 + if [[ "''${disk_healthy[$led]}" == "1" ]]; then + ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true + else + ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true + fi + fi + + # Trigger LED blink for activity + if [ -e "/sys/class/leds/$led/shot" ]; then + echo 1 > "/sys/class/leds/$led/shot" 2>/dev/null || true + else + ugreen_leds_cli "$led" -blink 100 100 2>/dev/null || true + sleep 0.05 + ugreen_leds_cli "$led" -on 2>/dev/null || true + fi + fi + + diskio_data["$led"]="$new_stat" + fi + done + + sleep ${refreshInterval} + done + ''; + + # Network activity monitoring script + netMonitorScript = pkgs.writeShellScript "ugreen-netdevmon" '' + set -euo pipefail + PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli pkgs.coreutils pkgs.iproute2 ]}:$PATH" + + INTERFACE="$1" + CHECK_INTERVAL=60 + + echo "Starting network monitoring on $INTERFACE" + + # Configure LED to trigger on network activity + led_path="/sys/class/leds/netdev" + + while true; do + # Check if interface is up + if ip link show "$INTERFACE" 2>/dev/null | grep -q "state UP"; then + # Link is up - set green + ugreen_leds_cli netdev -on -color ${colors.network} -brightness ${toString brightness} || true + + # Try to enable hardware trigger for activity indication + if [ -e "$led_path/device_name" ]; then + echo "$INTERFACE" > "$led_path/device_name" 2>/dev/null || true + fi + if [ -e "$led_path/rx" ]; then + echo 1 > "$led_path/rx" 2>/dev/null || true + fi + if [ -e "$led_path/tx" ]; then + echo 1 > "$led_path/tx" 2>/dev/null || true + fi + else + # Link is down - turn off + ugreen_leds_cli netdev -off || true + fi + + sleep $CHECK_INTERVAL + done + ''; + +in +{ + # Load i2c-dev kernel module for LED controller communication + boot.kernelModules = [ "i2c-dev" ]; + + # Install CLI tool + environment.systemPackages = [ pkgs.ugreen-leds-cli ]; + + # LED initialization service - runs once at boot + systemd.services.ugreen-leds-init = { + description = "Initialize UGREEN NAS LEDs"; + wantedBy = [ "multi-user.target" ]; + after = [ "local-fs.target" "systemd-modules-load.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${initLedsScript}"; + }; + }; + + # Disk activity monitoring service + systemd.services.ugreen-diskiomon = { + description = "UGREEN disk activity LED monitor"; + wantedBy = [ "multi-user.target" ]; + after = [ "ugreen-leds-init.service" "local-fs.target" ]; + requires = [ "ugreen-leds-init.service" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${diskMonitorScript}"; + Restart = "always"; + RestartSec = "5s"; + }; + }; + + # Network activity monitoring service (template for interface) + systemd.services."ugreen-netdevmon@" = { + description = "UGREEN network LED monitor for %i"; + after = [ "ugreen-leds-init.service" "network-online.target" ]; + requires = [ "ugreen-leds-init.service" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${netMonitorScript} %i"; + Restart = "always"; + RestartSec = "10s"; + }; + }; + + # Enable network monitoring for primary interface + systemd.services."ugreen-netdevmon@enp2s0" = { + wantedBy = [ "multi-user.target" ]; + }; +}