- C++ 55.8%
- Shell 44.2%
| psu-fan-controller.conf | ||
| psu-fan-controller.ino | ||
| psu-fan-ctl.service | ||
| psu-fan-ctl.sh | ||
| README.md | ||
PSU Fan Controller
Контроллер вентилятора блока питания на базе Arduino Nano с хост-сервером для Debian/Linux.
Проект состоит из двух компонентов:
- Arduino Nano (
.ino) — прошивка: чтение NTC-термисторов, управление ШИМ вентилятора, аварийная защита, звуковые оповещения - psu-fan-ctl (
.sh) — демон и CLI для Linux: чтение данных Arduino по USB-serial, sysfs-интерфейс, CSV-логирование, алерты, MQTT
Содержание
- Архитектура
- Схема подключения
- Прошивка Arduino
- Установка на Debian
- Конфигурация
- CLI-команды
- Sysfs-интерфейс
- Протокол serial
- Система аварийных уровней
- PID-регулирование
- Сохранение конфигурации (EEPROM)
- MQTT
- Структура файлов
Архитектура
┌─────────────────────┐ USB Serial ┌───────────────────┐
│ Arduino Nano │◄────── 115200 ────────►│ psu-fan-ctl │
│ │ JSON (2 сек) │ (bash daemon) │
│ A0 ─ 10k NTC │ │ │
│ A1 ─ 100k NTC │ │ /tmp/psu-fan/ │ ◄── sysfs
│ D3 ─ Fan PWM 25kHz │ │ sensors.csv │ ◄── лог
│ D6 ─ Piezo speaker │ │ alerts.log │ ◄── алерты
└─────────────────────┘ │ MQTT (опц.) │
└───────────────────┘
Принцип работы:
- Arduino читает два NTC-термистора, вычисляет температуру.
- Управление вентилятором ведётся по
max(temp1, temp2)— максимальной температуре среди исправных датчиков. - Arduino отправляет JSON-статус каждые 2 секунды по serial.
- Демон
psu-fan-ctlпарсит JSON, обновляет sysfs, логирует в CSV, проверяет пороги алертов. - CLI-команды передаются от клиента к демону через файловый релей, от демоны — к Arduino по serial.
Схема подключения
Arduino Nano
┌──────────────────┐
│ │
│ A0 ──── 10k ──── 5V (channel 1: 10k NTC)
│ A0 ──── NTC ──── GND 5V ──[10k]──┬── A0 ──[NTC]── GND
│ │ │
│ A1 ──── 100k ─── 5V (channel 2: 100k NTC)
│ A1 ──── NTC ──── GND 5V ──[100k]──┬── A1 ──[NTC]── GND
│ │ │
│ D3 ──── Fan gate (OC2B, PWM 25 kHz) │
│ D6 ──── Piezo speaker │
│ │
│ TX/RX ── USB Serial (115200 baud) │
└──────────────────┘
Датчики:
- Канал 1 (A0): NTC 10kOhm, B=3950, подтянутый резистор 10kOhm
- Канал 2 (A1): NTC 100kOhm, B=3950, подтянутый резистор 100kOhm
Вентилятор:
- Один вентилятор (M3016D или аналог), управление через D3 (Timer2, OC2B, 25 kHz)
- Оба канала имеют приоритет: применяется
max(duty_ch1, duty_ch2)
Прошивка Arduino
Требования
- Arduino IDE 1.8+ или arduino-cli
- Библиотеки: только стандартные AVR (
<avr/wdt.h>,<avr/eeprom.h>,<math.h>,<string.h>,<stdlib.h>)
Таймеры
| Таймер | Режим | Частота | Назначение |
|---|---|---|---|
| Timer1 | CTC, prescaler=64 | 10 kHz | Генерация тонов + системный тик (1 мс) |
| Timer2 | Fast PWM, TOP=79, prescaler=8 | 25 kHz | ШИМ вентилятора (OC2B → D3) |
Загрузка прошивки
arduino-cli compile --fqbn arduino:avr:nano psu-fan-controller.ino
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:nano psu-fan-controller.ino
Первичный запуск
При первом запуске (или после повреждения EEPROM) автоматически выполняется factory reset с дефолтными параметрами:
- min_temp=50.0°C, max_temp=75.0°C
- min_duty=25%, max_duty=100%
- hysteresis=2.0°C
- Режим: throttle
- PID: Kp=5.0, Ki=0.1, Kd=2.0
После загрузки прошивки Arduino выполняет самотест вентилятора (включение на 100% на 2 секунды) и воспроизводит мелодию boot.
Установка на Debian
Быстрая установка
chmod +x psu-fan-ctl.sh
sudo ./psu-fan-ctl.sh install
Это:
- Скопирует скрипт в
/usr/local/bin/psu-fan-ctl - Установит systemd-сервис
- Установит конфиг в
/etc/psu-fan-controller.conf(если отсутствует) - Создаст udev-правило для стабильного имени serial-порта (
/dev/psu-fan-arduino)
Запуск
sudo systemctl start psu-fan-ctl
sudo systemctl enable psu-fan-ctl # автозапуск при загрузке
Зависимости
jq— парсинг JSON (обязательно)python3— очистка DTR при реконнекте (опционально)mosquitto_pub— MQTT-публикация (опционально)
sudo apt install jq
Удаление
sudo psu-fan-ctl uninstall
Конфигурация
Файл: /etc/psu-fan-controller.conf
# Последовательный порт (пусто = автодетект /dev/ttyACM* или /dev/ttyUSB*)
SERIAL_PORT=""
# Скорость порта
BAUD=115200
# Интервал чтения (секунды)
INTERVAL=2
# Пороги алертов (°C) — на стороне хоста
ALERT_TEMP_WARN=80
ALERT_TEMP_CRIT=85
ALERT_TEMP_EMERG=90
# Методы оповещений
ALERT_WALL=1 # wall-сообщение при CRITICAL+
ALERT_SYSLOG=1 # запись в syslog
# Ретенция CSV-логов (дни, 0 = навсегда)
LOG_RETENTION_DAYS=30
# Необязательно: MQTT
# MQTT_HOST="localhost"
# MQTT_PORT=1883
# MQTT_USER=""
# MQTT_PASS=""
# MQTT_PREFIX="psu_fan"
Перезагрузка конфига
sudo kill -HUP $(cat /var/run/psu-fan-ctl.pid)
Автодетект порта
Если SERIAL_PORT пуст, демон ищет первый доступный порт:
/dev/ttyACM*(Arduino Nano)/dev/ttyUSB*(USB-serial адаптеры)
udev-правило создаёт стабильный симлинк /dev/psu-fan-arduino.
CLI-команды
Получение данных
| Команда | Описание |
|---|---|
psu-fan-ctl status |
Текущие показания (таблица) |
psu-fan-ctl watch [N] |
Мониторинг в реальном времени (обновление каждые N сек, по умолчанию 2) |
psu-fan-ctl info |
Информация о железе и конфиге Arduino |
psu-fan-ctl log |
Журнал событий Arduino (последние 32) |
psu-fan-ctl history [N] |
Последние N записей CSV (по умолчанию 20) |
Управление
| Команда | Описание |
|---|---|
psu-fan-ctl set <1|2> <0-100> |
Ручная установка duty вентилятора |
psu-fan-ctl auto <1|2> |
Автоматический режим |
psu-fan-ctl config <1|2> min_temp,max_temp,min_d,max_d,hyst |
Параметры управления |
psu-fan-ctl pid <1|2> Kp,Ki,Kd |
PID-коэффициенты |
psu-fan-ctl mode <1|2> <0|1> |
Режим: 0=throttle, 1=PID |
psu-fan-ctl test |
Самотест вентилятора (100% на 2 сек) |
psu-fan-ctl reset |
Factory reset EEPROM |
Системные
| Команда | Описание |
|---|---|
psu-fan-ctl daemon |
Запуск демона (в фоне) |
psu-fan-ctl rotate |
Ротация CSV-логов (при >1MB) |
psu-fan-ctl install |
Установка systemd-сервиса |
psu-fan-ctl uninstall |
Удаление systemd-сервиса |
Примеры
# Посмотреть текущий статус
psu-fan-ctl status
# Живой мониторинг каждую секунду
psu-fan-ctl watch 1
# Ручная установка вентилятора на 60%
psu-fan-ctl set 1 60
# Переключить в PID-режим
psu-fan-ctl mode 1 1
# Настроить пороги: min=50°C, max=80°C, duty 30-100%, гистерезис 20
psu-fan-ctl config 1 500,800,30,100,20
# Настроить PID
psu-fan-ctl pid 1 5.0,0.1,2.0
# Запустить демон
sudo psu-fan-ctl daemon
Sysfs-интерфейс
Демон экспортирует данные в виртуальную файловую систему /tmp/psu-fan/:
| Файл | Содержимое | Формат |
|---|---|---|
temp1_input |
Температура канала 1 | millidegrees (×1000) |
temp2_input |
Температура канала 2 | millidegrees (×1000) |
control_temp_input |
Управляющая температура (max) | millidegrees (×1000) |
fan1_input |
ШИМ duty вентилятора | % (0-100) |
fan1_mode |
Режим управления | auto / manual |
fan1_speed_rpm |
Скорость вентилятора (оценка) | RPM (duty × 30) |
emergency_level |
Уровень аварии | 0-5 |
status |
Текстовый статус | normal/watch/warning/critical/emergency/fatal |
uptime |
Время работы Arduino | секунды |
connected |
Состояние serial-соединения | 0/1 |
timestamp |
Unix timestamp последнего обновления | секунды |
Пример чтения
# Температура канала 1 в °C
awk "BEGIN { printf \"%.1f\", $(cat /tmp/psu-fan/temp1_input) / 1000 }"
# Статус аварии
cat /tmp/psu-fan/status
Протокол serial
Все данные передаются в формате JSON, 115200 baud.
Arduino → Хост (каждые 2 сек)
{"t1":72.3,"t2":68.5,"tc":72.3,"f1":45,"f2":0,"m1":1,"m2":1,"e":0,"s1":1,"s2":1,"u":3600,"d":0,"b":5}
| Поле | Описание |
|---|---|
t1 |
Температура канала 1 (°C) |
t2 |
Температура канала 2 (°C) |
tc |
Управляющая температура (max t1, t2) |
f1, f2 |
Duty вентилятора каналов 1 и 2 (%) |
m1, m2 |
Режим каналов (0=manual, 1=auto) |
e |
Глобальный аварийный уровень (0-5) |
s1, s2 |
Состояние датчиков (0=ошибка, 1=ОК) |
u |
Время работы (секунды) |
d |
Режим управления (0=throttle, 1=PID) |
b |
Счётчик загрузок |
Хост → Arduino (команды)
Команда отправляется одной строкой с \n.
| Команда | Формат | Описание |
|---|---|---|
F |
F<ch>:<duty> |
Ручной duty (ch=1|2, duty=0-100) |
M |
M<ch>:<0|1> |
Режим: 0=manual, 1=auto |
C |
C<ch>:mn,mx,mi,ma,hy |
Конфигурация: min_temp, max_temp, min_duty, max_duty, hyst |
P |
P<ch>:kp,ki,kd |
PID-коэффициенты |
X |
X<ch>:<0|1> |
Режим регулирования: 0=throttle, 1=PID |
T |
T |
Самотест вентилятора |
I |
I |
Информация о hardware |
G |
G |
Принудительная выдача статуса |
E |
E |
Выгрузка журнала событий |
Z |
Z |
Factory reset EEPROM |
Журнал событий (E)
{"events":[{"t":120,"e":0,"c":255,"v":5},{"t":125,"e":12,"c":0,"v":723}]}
Поле e (EventType) |
Значение |
|---|---|
| 0 | EVT_BOOT |
| 1 | EVT_CONFIG_CHANGE |
| 2 | EVT_EEPROM_LOAD |
| 3 | EVT_EEPROM_RESET |
| 4 | EVT_SENSOR_FAIL |
| 5 | EVT_SENSOR_RECOVER |
| 6 | EVT_FAN_FAIL |
| 7 | EVT_FAN_RECOVER |
| 8 | EVT_TEMP_WATCH |
| 9 | EVT_TEMP_WARN |
| 10 | EVT_TEMP_CRIT |
| 11 | EVT_TEMP_EMERG |
| 12 | EVT_THERMAL_RUNAWAY |
| 13 | EVT_EMERG_OVERRIDE |
| 14 | EVT_FAN_TEST |
| 15 | EVT_FAN_TEST_DONE |
| 16 | EVT_WDT_RESET |
Система аварийных уровней
| Уровень | Значение | Порог (от max_temp) | При 75°C max | Действие |
|---|---|---|---|---|
| NORMAL | 0 | < WATCH | < 75°C | Обычное управление |
| WATCH | 1 | +0 | 75°C | Короткий писк |
| WARNING | 2 | +50 (5.0°C) | 80°C | Двойной писк, повтор каждые 10 сек |
| CRITICAL | 3 | +100 (10.0°C) | 85°C | Тройной восходящий писк, принудительный max duty, auto-override |
| EMERGENCY | 4 | +150 (15.0°C) | 90°C | Срочный писк, повтор каждые 3 сек, принудительно 100% duty |
| FATAL | 5 | +200 (20.0°C) | 95°C | Долгий сигнальный писк, повтор каждые 2 сек, принудительно 100% duty |
Дополнительные условия:
- Датчик неисправен → CRITICAL (вентилятор на максимум)
- Thermal runaway (temp растёт >5.0°C/мин при duty ≥90%) → EMERGENCY
- Fan failure (duty ≥80% >30 сек, но температура растёт >1.0°C/мин) → WARNING
- Emergency override: при CRITICAL+ принудительно включается auto-режим
Звуки
| Уровень | Мелодия |
|---|---|
| Boot | C5→E5→G5→C6 (восходящий арпеджио) |
| WATCH | Одиночный мягкий писк |
| WARNING | Двойной писк |
| CRITICAL | Тройной восходящий (C5→E5→G5) |
| EMERGENCY | Быстрый тревожный (C6→G5→C6→G5) |
| FATAL | Долгий чередующийся (C6, пауза, C6, пауза, G5, пауза, G5) |
PID-регулирование
При mode=1 (PID) управляющий сигнал вычисляется классическим PID-регулятором:
error = temperature - setpoint
setpoint = (min_temp + max_temp) / 2
output = Kp × error + Ki × ∫error + Kd × d(error)/dt
Дефолтные коэффициенты: Kp=5.0, Ki=0.1, Kd=2.0
Ограничения:
- Anti-windup: интегральная составляющая ограничена ±1000
- Выход ограничен 0-100%
- При temperature < min_temp: duty ≥ min_duty (если duty > 0)
Throttle (по умолчанию)
При mode=0 (throttle) duty вычисляется линейной интерполяцией:
duty = min_duty + (temp - min_temp) × (max_duty - min_duty) / (max_temp - min_temp)
С гистерезисом для предотвращения дребезга:
- Включение: при достижении min_temp + hysteresis
- Выключение: при падении ниже min_temp - hysteresis/2
Сохранение конфигурации (EEPROM)
Настройки хранятся в EEPROM Arduino с защитой CRC16- checksum.
| Поле | Размер | Описание |
|---|---|---|
| magic | uint16 | 0xA55A (валидность данных) |
| version | uint8 | 3 (версия формата) |
| boot_count | uint16 | Счётчик загрузок |
| ch1/ch2_* | разные | Параметры каналов (min_temp, max_temp, duty, hyst, PID) |
| crc | uint16 | CRC16 по всем полям |
Валидация при загрузке: magic, version, CRC, sanity-check (max_temp > min_temp, max_duty > 0).
При любой ошибке EEPROM выполняется factory reset. Factory reset также доступен через команду Z.
MQTT
Опциональная интеграция с MQTT (требует mosquitto-clients).
Публикация происходит каждые 60 секунд в топик {MQTT_PREFIX}/status:
{"t1":72.3,"t2":68.5,"tc":72.3,"f1":45,"e":0,"u":3600,"mode":"auto"}
Настройка в конфиге:
MQTT_HOST="localhost"
MQTT_PORT=1883
MQTT_USER=""
MQTT_PASS=""
MQTT_PREFIX="psu_fan"
Структура файлов
psu-fan-controller/
├── psu-fan-controller.ino # Прошивка Arduino Nano
├── psu-fan-ctl.sh # Демон и CLI для Linux
├── psu-fan-controller.conf # Конфигурация (устанавливается в /etc/)
└── psu-fan-ctl.service # Systemd unit-файл
Файловая система при установке
/etc/psu-fan-controller.conf # Конфигурация
/etc/systemd/system/psu-fan-ctl.service # Systemd сервис
/etc/udev/rules.d/99-psu-fan.rules # Правила udev
/usr/local/bin/psu-fan-ctl # Скрипт
/var/lib/psu-fan-controller/
├── sensors.csv # CSV-лог температур
├── alerts.log # Лог алертов
└── stats # Min/max статистика
/tmp/psu-fan/
├── temp1_input # Температура канала 1
├── temp2_input # Температура канала 2
├── control_temp_input # Управляющая температура
├── fan1_input # Duty вентилятора
├── fan1_mode # Режим
├── fan1_speed_rpm # Скорость (оценка)
├── emergency_level # Уровень аварии
├── status # Текстовый статус
├── connected # Serial-соединение
├── uptime # Время работы
├── timestamp # Unix timestamp
├── command # Релей команд (CLI → daemon)
└── response # Релей ответов (daemon → CLI)
/var/run/
├── psu-fan-ctl.pid # PID демона
└── psu-fan-ctl.lock # Lock-файл (singleton)