#!/bin/sh
# wd-mcu: Lüfter- und Power-LED-Steuerung für WD My Cloud EX2 Ultra unter OpenWrt
# Mikrocontroller an /dev/ttyS1, 19200 8N1, 7-Byte-Pakete FA .. FB
# Protokoll nach github.com/nmaas87/mcm-fancontrol (WD MCMgen2/Ex2U)
# Stand 2026-10-08, BeDa

TTY=/dev/ttyS1
RX=/tmp/wd-mcu.rx
INTERVAL=30
FANTEST_EVERY=86400   # Funktionstest bei stehendem Lüfter (Sekunden)

# Schwellen je Sensor in °C: aus (unter) / an (ab, Stufe 3) / voll (ab, Stufe 9)
HDD_OFF=38;  HDD_ON=40;  HDD_FULL=50
CPU_OFF=65;  CPU_ON=70;  CPU_FULL=75
CASE_OFF=40; CASE_ON=43; CASE_FULL=50
PHY_OFF=55;  PHY_ON=60;  PHY_FULL=75

# Noctua NF-A4x10: Stufe 1/2 steht, Stufe 3 bleibt beim Herunterregeln stehen (08.10.) -> Laufminimum 4
MINRUN=4
FAN_HEX_0=00; FAN_HEX_3=40; FAN_HEX_4=50; FAN_HEX_5=60
FAN_HEX_6=80; FAN_HEX_7=a0; FAN_HEX_8=c0; FAN_HEX_9=d2

# Umrechnung Gehäusesensor (Rohwert -> °C), aus mcm-fancontrol
THERMAL_TABLE="116 115 114 113 112 111 110 109 108 107 106 105 104 103 102 101 100 99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 79 78 77 76 75 74 73 72 71 70 69 68 67 66 65 65 64 63 62 62 61 61 60 59 58 58 57 56 56 55 54 54 53 52 52 51 51 50 49 49 48 48 47 47 46 46 45 44 44 43 43 42 42 41 41 40 40 39 39 39 38 38 37 37 36 36 35 35 34 34 33 33 33 32 32 31 31 30 30 30 29 29 28 28 27 27 27 27 26 25 25 25 24 24 23 23 37 27 27 25 25 25 24 24 23 23 22 22 22 21 21 20 20 20 19 19 18 18 18 17 17 16 16 16 15 15 14 14 14 13 13 12 12 12 11 11 10 10 9 9 9 8 8 7 7 7 6 6 5 5 4 4 4 3 3 2 2 1 1 0 0 0 0 0 0 0 0"

log() { logger -t wd-mcu "$*"; }

# Paket senden, Antwortbytes (hex, Leerzeichen) ausgeben
mcu() {
	: > "$RX"
	printf "$1" > "$TTY"
	sleep 1
	hexdump -v -e '1/1 "%02x "' "$RX"
}

# Wert aus Antwort fa <a> <b> 00 00 <wert> fb holen
mcu_val() { echo "$1" | awk -v a="$2" -v b="$3" '{for(i=1;i<=NF-6;i++) if($i=="fa"&&$(i+1)==a&&$(i+2)==b&&$(i+6)=="fb"){print $(i+5);exit}}'; }

set_fan() {
	eval hex=\$FAN_HEX_$1
	mcu "\\xfa\\x02\\x00\\x$hex\\x00\\x00\\xfb" >/dev/null
	CUR=$1
}
kick_fan() { set_fan 9; sleep 3; set_fan "$1"; }
# Drehzahl-Rohwert (U/min = 300000/Rohwert); unplausibel schnelle Werte (< 0x3c, > 5000 U/min) verwerfen
fan_raw() {
	for try in 1 2 3; do
		v=$(mcu_val "$(mcu '\xfa\x02\x01\x00\x00\x00\xfb')" 02 01)
		[ -z "$v" ] && continue
		[ "$v" = "00" ] && { echo 00; return; }
		[ $((0x$v)) -ge 60 ] && { echo $v; return; }
	done
	echo "$v"
}
# Power-LED (Kommando 0x26 laut mcm-daemon): 11 blau an, 41 blau pulsieren, 24 rot blinken, 22 orange blinken
led() { mcu "\\xfa\\x26\\x00\\x$1\\x00\\x01\\xfb" >/dev/null; }
rpm() { [ -z "$1" ] || [ "$1" = "00" ] && { echo 0; return; }; [ $((0x$1)) -lt 60 ] && echo "?" || echo $((300000 / 0x$1)); }

# Drehzahl prüfen; steht er, Anlaufhilfe und erneut messen; Ergebnis setzt FAULT und LED
check_fan() { # Sollstufe
	raw=$(fan_raw)
	if [ "$raw" = "00" ] || [ -z "$raw" ]; then
		log "Lüfter steht trotz Stufe $1 (Rohwert ${raw:-keine Antwort}) -> Anlaufhilfe"
		kick_fan $1; sleep 3; raw=$(fan_raw)
	fi
	if [ "$raw" = "00" ] || [ -z "$raw" ]; then
		if [ $FAULT -eq 0 ]; then FAULT=1; logger -p daemon.err -t wd-mcu "LÜFTER DEFEKT: keine Drehzahl trotz Anlaufhilfe, Power-LED blinkt rot"; fi
		set_fan 9
	else
		if [ $FAULT -eq 1 ]; then FAULT=0; log "Lüfter wieder in Ordnung ($(rpm $raw) U/min)"; fi
	fi
	LASTTEST=$(date +%s)
}

case_temp() {
	r=$(mcu_val "$(mcu '\xfa\x03\x08\x00\x00\x00\xfb')" 03 08)
	[ -n "$r" ] || return
	echo "$THERMAL_TABLE" | awk -v i=$((0x$r + 1)) '{if(i<=NF) print $i}'
}

# Platten-LEDs je Slot (ata1/ata2): gesperrt (RAID ohne geöffnetes LUKS bzw. Partition frei) violett langsam blinkend,
# entsperrt blau mit Zugriffsanzeige, von mdadm als defekt markiert rot dauerhaft, leer aus
slot_zustand() {
	for b in /sys/block/sd*; do
		readlink -f "$b" | grep -q "/ata$1/" || continue
		for p in "$b"/"${b##*/}"[0-9]*; do
			h=$(ls "$p/holders" 2>/dev/null | head -1)
			[ -n "$h" ] || continue
			case $h in
			md*)	case "$(cat /sys/block/$h/md/dev-${p##*/}/state 2>/dev/null)" in *faulty*) echo defekt; return;; esac
				[ -n "$(ls /sys/block/$h/holders 2>/dev/null)" ] && { echo offen; return; } ;;
			*)	echo offen; return ;;
			esac
		done
		echo gesperrt; return
	done
	echo leer
}
# Violett = Rot+Blau gleichzeitig; Kernel-Timer laufen nicht phasengleich, daher eigener Blinker
VIOLETT=/tmp/wd-mcu.violett
blinker() {
	L=/sys/class/leds
	while :; do
		s=$(cat $VIOLETT 2>/dev/null)
		if [ -z "$s" ]; then sleep 2; continue; fi
		for i in $s; do echo 1 > $L/red:hdd$i/brightness; echo 1 > $L/blue:hdd$i/brightness; done
		sleep 1
		for i in $s; do echo 0 > $L/red:hdd$i/brightness; echo 0 > $L/blue:hdd$i/brightness; done
		sleep 1
	done
}
led_slot() { # slot zustand
	r=/sys/class/leds/red:hdd$1; bl=/sys/class/leds/blue:hdd$1
	case $2 in
	gesperrt)
		echo none > $r/trigger; echo none > $bl/trigger
		grep -qw $1 $VIOLETT 2>/dev/null || echo $1 >> $VIOLETT ;;
	defekt)
		[ -f $VIOLETT ] && { grep -vw $1 $VIOLETT > $VIOLETT.n; mv $VIOLETT.n $VIOLETT; sleep 2; }
		echo none > $bl/trigger; echo 0 > $bl/brightness; echo none > $r/trigger; echo 1 > $r/brightness
		logger -p daemon.err -t wd-mcu "Slot $1: Platte von mdadm als defekt markiert, LED rot" ;;
	*)
		[ -f $VIOLETT ] && { grep -vw $1 $VIOLETT > $VIOLETT.n; mv $VIOLETT.n $VIOLETT; sleep 2; }
		echo none > $r/trigger; echo 0 > $r/brightness; echo ata$1 > $bl/trigger ;;
	esac
}

hwmon_by_name() { for h in /sys/class/hwmon/hwmon*; do [ "$(cat $h/name 2>/dev/null)" = "$1" ] && echo $h; done; }
milli() { [ -r "$1" ] && awk '{printf "%d", $1/1000}' "$1"; }

# Bedarf eines Sensors: 0 (aus), 3..9; bei laufendem Lüfter Hysterese bis Ausschaltschwelle
demand() { # temp off on full
	awk -v t="$1" -v off="$2" -v on="$3" -v full="$4" -v run="$CUR" 'BEGIN{
		if (t>=full) d=9
		else if (t>=on) { d=3+int(6*(t-on)/(full-on)); if(d>8) d=8 }
		else if (run>0 && t>=off) d=3
		else d=0
		print d }'
}

cleanup() {
	set_fan 9; led 22
	kill $BLINKPID 2>/dev/null; rm -f $VIOLETT
	led_slot 1 offen; led_slot 2 offen
	log "beendet: Lüfter Stufe 9, Power-LED blinkt orange"
	kill $CATPID 2>/dev/null
	exit 0
}

stty -F "$TTY" 19200 cs8 -cstopb -parenb raw -echo -ixon -ixoff -crtscts || { log "stty fehlgeschlagen"; exit 1; }
cat "$TTY" >> "$RX" &
CATPID=$!
rm -f $VIOLETT; : > $VIOLETT
blinker &
BLINKPID=$!
trap cleanup INT TERM

mcu '\xfa\x03\x01\x00\x00\x00\xfb' >/dev/null   # Gerät bereit
led 11
CUR=0; FAULT=0; LASTTEST=0; LEDCUR=11
kick_fan 9
check_fan 9
log "gestartet: Gerät bereit, Power-LED blau, Lüfter Stufe 9 bis zur ersten Messung ($(rpm $raw) U/min)"

n=0
while true; do
	missing=""
	tc=$(case_temp)
	tcpu=$(milli /sys/class/thermal/thermal_zone0/temp)
	tphy=$(milli "$(hwmon_by_name f1072004mdiomii00)/temp1_input")
	# Platten: schlafende nicht abfragen (würde sie ggf. wecken), gelten als kühl
	hdds=""; hddinfo=""; ndisk=0; nsleep=0
	for b in /sys/block/sd*; do
		dev=${b##*/}
		ndisk=$((ndisk + 1))
		if command -v smartctl >/dev/null; then
			smartctl -n standby -i /dev/$dev >/dev/null 2>&1
			[ $? -eq 2 ] && { hddinfo="$hddinfo $dev=schläft"; nsleep=$((nsleep + 1)); continue; }
		fi
		for h in $b/device/hwmon/hwmon*; do
			[ -e "$h/temp1_input" ] || continue   # z. B. USB-Stick ohne Sensor
			t=$(milli $h/temp1_input)
			[ -n "$t" ] && { hdds="$hdds $t"; hddinfo="$hddinfo $dev=$t"; } || missing="$missing $dev"
		done
	done

	want=0
	[ -n "$tc" ]   && d=$(demand $tc $CASE_OFF $CASE_ON $CASE_FULL) || { missing="$missing Gehäuse"; d=9; }; [ $d -gt $want ] && want=$d
	[ -n "$tcpu" ] && d=$(demand $tcpu $CPU_OFF $CPU_ON $CPU_FULL) || { missing="$missing CPU"; d=9; };   [ $d -gt $want ] && want=$d
	[ -n "$tphy" ] && d=$(demand $tphy $PHY_OFF $PHY_ON $PHY_FULL) || { missing="$missing PHY"; d=9; };   [ $d -gt $want ] && want=$d
	for t in $hdds; do d=$(demand $t $HDD_OFF $HDD_ON $HDD_FULL); [ $d -gt $want ] && want=$d; done
	[ -n "$missing" ] && want=9

	# beim Abregeln höchstens eine Stufe pro Durchlauf; 3 -> 0 direkt
	if [ $want -lt $CUR ] && [ $want -gt 0 ]; then want=$((CUR - 1)); fi
	[ $want -gt 0 ] && [ $want -lt $MINRUN ] && want=$MINRUN
	[ $FAULT -eq 1 ] && want=9   # bei Lüfterfehler weiter Vollgas versuchen

	status="Gehäuse=${tc:-?} CPU=${tcpu:-?} PHY=${tphy:-?} HDD:${hddinfo:- keine}"
	[ -n "$missing" ] && log "Sensor fehlt:$missing -> Stufe 9"

	if [ $want -ne $CUR ]; then
		if [ $CUR -eq 0 ] && [ $want -gt 0 ]; then kick_fan $want; else set_fan $want; fi
		log "Stufe $want ($status)"
	fi

	if [ $CUR -gt 0 ]; then
		check_fan $CUR
	elif [ $(( $(date +%s) - LASTTEST )) -ge $FANTEST_EVERY ]; then
		kick_fan $MINRUN; sleep 5; check_fan $MINRUN
		log "Funktionstest bei Stillstand: $(rpm $raw) U/min"
		[ $FAULT -eq 0 ] && set_fan 0
	fi

	# Power-LED: Lüfterfehler rot blinkend > alle Platten schlafen blau pulsierend > blau an
	if [ $FAULT -eq 1 ]; then ledwant=24
	elif [ $ndisk -gt 0 ] && [ $nsleep -eq $ndisk ]; then ledwant=41
	else ledwant=11; fi
	if [ $ledwant -ne $LEDCUR ]; then
		led $ledwant; LEDCUR=$ledwant
		case $ledwant in 24) log "Power-LED rot blinkend (Lüfterfehler)";; 41) log "Power-LED blau pulsierend (alle Platten im Ruhezustand)";; 11) log "Power-LED blau";; esac
	fi

	for s in 1 2; do
		z=$(slot_zustand $s); eval alt=\$SLOT_$s
		if [ "$z" != "$alt" ]; then led_slot $s $z; eval SLOT_$s=$z; log "Slot $s: $z"; fi
	done

	n=$((n + 1))
	[ $((n % 20)) -eq 1 ] && log "Status: Stufe $CUR $status Drehzahl=$(rpm $raw)U/min$([ $FAULT -eq 1 ] && echo " LÜFTERFEHLER")"
	sleep $INTERVAL & wait $!
done
