Всем привет!
Моя основная система — Linux (Arch). Мне она очень нравится, но относительно недавно я обновил процессор и решил заодно поменять кулер. Выбор пал на Ocypus Delta A62 EX Digital. У него есть простая, но симпатичная фича: небольшой экранчик, на котором можно показывать температуру процессора или видеокарты.
У Ocypus есть официальное приложение для Windows, а для Linux — нет. Поэтому я решил написать своё. В то время я как раз изучал Go, и он почти идеально подходит под такую задачу, но, честно говоря, это было ещё и желанием потренироваться в языке. К слову, я думаю когда‑нибудь переписать всё на C или Rust, но из‑за большого объёма учёбы времени на это пока нет.
Так и начался проект ocyd.
-
Репозиторий: github.com/Dilikel/ocyd
-
Лицензия: GPLv3
Как устроен путь от идеи до рабочего демона
1. Что вообще такое этот экранчик
Первым делом нужно было понять, как кулер выглядит для системы. Выяснилось быстро: экран определяется как HID‑устройство с VID:PID = 1a2c:434d и названием «China Resource Semico Co., Ltd USB Gaming Keyboard».
Bus 001 Device 005: ID 1a2c:434d China Resource Semico Co., Ltd USB Gaming Keyboard
Видимо, чтобы не тратить деньги на собственные VID и PID, Ocypus просто взяли чужую платформу от какой‑то игровой клавиатуры.
2. Реверс‑инжиниринг протокола
Дальше нужно было понять, какие данные отправляет официальное приложение и как они меняются. Для этого я использовал Wireshark и драйвер USBPcap на Windows. Зная VID:PID, достаточно легко отфильтровать трафик именно этого устройства.
Проанализировав байты, я понял, что официальное приложение отправляет пакет из 64 байт:
Байт (с нуля)
Значение
0
Report ID = 0x07
1, 2
0xFF, 0xFF
3, 4, 5
цифры температуры: сотни, десятки, единицы
6—63
нули
То есть температура отправляется очень «в лоб»: не одним числом, а тремя отдельными цифрами. На этом весь реверс‑инжиниринг, больше ничего и не потребовалось:)
В коде отправка выглядит так:
// SendTemp раскладывает температуру на 3 цифры и пишет 64-байтный репорт.func (d *Display) SendTemp(temp int) error {digits := decompose(temp)copy(d.buf[3:6], digits[:])n, err := d.dev.Write(d.buf[:])// ...}func decompose(temp int) [3]byte {if temp 999 {temp = 999}return [3]byte{byte(temp / 100),byte((temp % 100) / 10),byte(temp % 10),}}
Для работы с HID я взял библиотеку sstallion/go-hid. Демон открывает устройство по VID:PID и раз в секунду шлёт актуальную температуру.
3. Сам демон
Зная, что отправлять, осталось написать, собственно, демон. Как и в официальном приложении, я решил дать пользователю выбор:
-
температура CPU или GPU;
-
градусы Цельсия или Фаренгейта;
-
собственный VID:PID устройства.
Последний пункт важен. Я могу ручаться только за свою версию кулера: другие модели, возможно, работают, возможно, нет. А хардкодить VID:PID вообще плохая идея.
4. Где в Linux взять температуру
Температура процессора в Linux лежит в /sys/class/hwmon/hwmon*/temp*_input. Осталось понять, чем именно являются эти * в каждом конкретном случае. Для этого я описал базовые драйверы и метки их датчиков, по которым демон находит нужный файл:
Драйверы
Метки
CPU
k10temp, coretemp, zenpower, amd_energy
Tctl, Tdie, Package id 0, Package id 1
GPU
amdgpu, nouveau, nvidia, i915, xe
edge, junction, hotspot, GPU
Алгоритм простой: перебираем hwmon*/name, ищем драйвер из списка, внутри него ищем temp*_label с подходящей меткой и читаем соответствующий temp*_input. Если метки не нашлось, берём temp1_input. Ядро отдаёт значения в милли‑градусах Цельсия, поэтому результат делится на 1000.
cpuTarget = sensorTarget{Drivers: []string{"k10temp", "coretemp", "zenpower", "amd_energy"},Labels: []string{"Tctl", "Tdie", "Package id 0", "Package id 1"},}
Для видеокарт всё то же самое, кроме проприетарного драйвера NVIDIA: он не отдаёт датчик через hwmon. Для него демон использует библиотеку NVML (через go-nvml): сначала пробуется sysfs, и только если там ничего не нашлось, включается NVML.
Не буду углубляться в реализацию: весь код открыт, вы можете его посмотреть, разобраться и подсказать мне, если я что‑то делаю неправильно.
5. Доступ к устройству без root: udev
Чтобы демон мог писать в HID‑устройство, нужно правило udev. Без него hidraw‑узел принадлежит root, а запускать демон от root не хочется.
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1a2c", ATTRS{idProduct}=="434d", MODE="0666"
Здесь 1a2c:434d — это VID:PID именно Ocypus Delta A62 EX Digital. У других моделей он может быть другим, поэтому его можно поменять и в правиле, и в конфиге. Стоит помнить и о компромиссе: MODE="0666" открывает узел этого устройства всем локальным пользователям. Для экрана, который просто показывает число, я счёл это приемлемым.
6. systemd, конфиг и ocydctl
Демон запускается как пользовательский systemd‑сервис, так что root для него не нужен:
systemctl --user enable --now ocyd.servicejournalctl --user -u ocyd.service -f
Настройки лежат в ~/.config/ocyd/config.toml:
[display]source = 'CPU' # 'CPU' или 'GPU'unit = 'C' # 'C' или 'F'[device]vendor_id = '0x1a2c'product_id = '0x434d'
Чтобы не править файл руками, я сделал небольшую утилиту ocydctl:
ocydctl -source GPU -unit Focydctl -vendor 0x1a2c -product 0x434dsystemctl --user restart ocyd.service
VID и PID принимаются и в hex, и в десятичном виде. Демон читает конфиг только при старте, поэтому после изменений его нужно перезапустить.
7. Установка и удаление одной командой
Чтобы человеку не приходилось руками раскладывать файлы, я написал install.sh и uninstall.sh.
install.sh:
-
ищет готовые бинарники: сначала рядом со скриптом (если вы скачали архив релиза), затем в
bin/(если склонировали репозиторий). Если их нет, соберёт сам; -
спрашивает, оставить ли VID:PID по умолчанию (
1a2c:434d, Delta A62 EX Digital) или ввести свои; -
копирует бинарники в
/usr/bin, а udev‑правило в/etc/udev/rules.d; -
создаёт конфиг, если его ещё нет;
-
спрашивает, нужен ли systemd. Если да, кладёт сервис в
~/.config/systemd/user/и спрашивает, включить ли автозапуск и запустить ли сразу.
uninstall.sh делает обратное: останавливает и отключает сервис, удаляет юнит, бинарники и udev‑правило, а про конфиг спрашивает отдельно.
Установка
Из архива релиза (сборка не нужна):
tar -xzf ocyd_*_linux_amd64.tar.gzcd ocyd_*_linux_amd64./install.sh
Из исходников:
git clone https://github.com/Dilikel/ocyd.gitcd ocyd./install.sh
Для сборки понадобятся Go, gcc и заголовки libudev (libudev-dev в Debian/Ubuntu, systemd-libs в Arch).
Итог
На данный момент я реализовал:
-
демон, который каждую секунду отправляет температуру CPU или GPU на экран кулера и при этом уже сейчас потребляет всего около 10 МБ оперативной памяти;
-
поддержку датчиков AMD, Intel и NVIDIA, включая проприетарный драйвер через NVML;
-
выбор источника и единиц измерения (°C / °F) и настраиваемые VID:PID;
-
утилиту
ocydctlдля изменения настроек из командной строки; -
пользовательский systemd‑сервис, udev‑правило и скрипты установки и удаления;
-
готовые релизы через GoReleaser и двуязычный README (русский и английский).
Важная оговорка: проект на ранней альфа‑стадии. Проверено только на моём кулере, Ocypus Delta A62 EX Digital. Юнит‑тестов пока нет, а код ещё не оптимизирован и не отполирован.
Что планирую дальше:
-
добавить тесты (конфиг, поиск датчиков на поддельном дереве sysfs, HID‑драйвер на мок‑устройстве);
-
сделать код чище и лучше: рефакторинг, аккуратная обработка ошибок и структура;
-
выпустить пакеты в AUR:
ocyd(сборка из исходников) иocyd-bin(готовый релиз); -
поддержать больше моделей кулеров.
Помогите проверить другие модели
Если у вас другой кулер Ocypus (или вы просто нашли у меня ошибку), буду рад любой обратной связи:
-
узнайте VID:PID командой
lsusb; -
поменяйте их в конфиге и в udev‑правиле (или просто запустите
./install.shи введите свои значения); -
напишите в issue, даже если не заработало: укажите точную модель, дистрибутив, VID:PID и логи
journalctl --user -u ocyd.service.
Также буду благодарен за code review: если вы видите, как сделать лучше, пишите в комментариях или в issue.
Спасибо, что дочитали!
Источник: habr.com