Temperature-based fan control for a DIY disk station: 4x 3.5" HDDs in a USB enclosure, cooled by 2x Noctua 5V fans (Y-splitter) driven from an Arduino Uno's PWM pin 9.
arduino/diskfan_controller/- sketch that generates true 25kHz PWM on pin 9 (Noctua spec) and takes plaintext speed commands over USB serial.daemon/diskfanspeedd- Linux daemon. Pollssmartctlfor each configured disk everypoll_intervalseconds, takes the highest temp, maps it to a fan speed with a linear curve, and sends it to the Arduino. Exposes a control socket for the CLI.cli/diskfanspeed- talks to the daemon over its control socket.
- Both Noctua 5V fans' PWM wires join at a Y-splitter, single PWM signal into Arduino pin 9.
- Fan +5V/GND powered from a suitable 5V source (not necessarily the Arduino's own 5V rail if both fans draw more current than it can supply).
- Arduino connected to the host via USB (shows up as
/dev/ttyACM0).
- Open
arduino/diskfan_controller/diskfan_controller.inoin the Arduino IDE (orarduino-cli). - Flash to the Uno.
- Confirm it enumerates as
/dev/ttyACM0on the host (dmesg | tailafter plugging in).
Protocol: plaintext integer percent (0-100) + newline over serial sets
the duty cycle; ? queries it. If the Arduino gets no command for 90
seconds (daemon crashed, USB unplugged, etc.) it fails safe to 100% -
same default it boots with before the daemon connects.
Requires Python 3, pyserial, and smartmontools:
sudo apt install python3-serial smartmontools
Install the daemon and CLI:
sudo install -m 755 daemon/diskfanspeedd /usr/local/sbin/diskfanspeedd
sudo install -m 755 cli/diskfanspeed /usr/local/bin/diskfanspeed
sudo mkdir -p /etc/diskfanspeed
sudo install -m 644 config/diskfanspeedd.conf /etc/diskfanspeed/diskfanspeedd.conf
sudo install -m 644 systemd/diskfanspeedd.service /etc/systemd/system/diskfanspeedd.service
Create the socket group and add yourself to it if you want to run the CLI
without sudo:
sudo groupadd -f diskfanspeed
sudo usermod -aG diskfanspeed "$USER"
(log out/in for the group change to apply)
Edit /etc/diskfanspeed/diskfanspeedd.conf:
disks.devices- the 4 disk device paths (check withlsblk/ls /dev/disk/by-id/; use stable/dev/disk/by-id/...paths if your enclosure doesn't guarantee/dev/sdXordering across reboots).disks.smart_device_type- usuallysatfor USB-SATA bridges; ifsmartctl -a -d sat /dev/sdXdoesn't return temps, trysmartctl --scanor-d sat,12/-d usbjmicronvariants and set a per-disk[disk:/dev/sdX]override.fan_curve- tunemin_temp_c/max_temp_c/min_pwm/max_pwmto taste.min_pwmshould stay high enough that the 5V Noctuas spin up reliably (they don't like very low duty cycles) - 20% is a reasonable starting floor, raise it if fans stall or hum at idle.
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now diskfanspeedd
diskfanspeed status # mode, current fan %, max temp, per-disk temps
diskfanspeed auto # temperature-based control (default)
diskfanspeed full # force fans to 100%
diskfanspeed off # manually turn the fan off
off is only accepted while every configured disk is currently asleep; the
daemon rejects the command otherwise. If any disk wakes up while the fan is
manually off, the daemon automatically reverts to auto mode so an active
disk is never left without cooling.
- Two of the four disks are Jellyfin media disks (only spin up during
streaming) and two are Time Machine targets (wake every couple of
hours). The daemon checks disk power state (
smartctl -n standby) before reading SMART data, so it won't wake a sleeping disk just to poll its temperature - it reuses the last known temperature for that disk instead. If every disk is asleep, the daemon holds the last fan speed forfan_curve.idle_delay_s(default 3600s = 1 hour) before falling back tofan_curve.idle_pwm(fans off by default), so a disk napping briefly doesn't spin the fans down and back up. The fan can also be turned off manually withdiskfanspeed offonce every disk is already asleep. - State (last known temps) persists to
state.jsonunder/var/lib/diskfanspeeddso a daemon restart doesn't momentarily forget a sleeping disk's last reading.