Всем привет!
Моя основная система — 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 байт:
Байт (с нуля) | Значение |
|---|---|
| Report ID = |
|
|
| цифры температуры: сотни, десятки, единицы |
| нули |
То есть температура отправляется очень «в лоб»: не одним числом, а тремя отдельными цифрами. На этом весь реверс‑инжиниринг, больше ничего и не потребовалось:)
В коде отправка выглядит так:
// 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 < 0 {
temp = 0
}
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 |
|
|
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.service
journalctl --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 F
ocydctl -vendor 0x1a2c -product 0x434d
systemctl --user restart ocyd.service
VID и PID принимаются и в hex, и в десятичном виде. Демон читает конфиг только при старте, поэтому после изменений его нужно перезапустить.
7. Установка и удаление одной командой
Чтобы человеку не приходилось руками раскладывать файлы, я написал install.sh и uninstall.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.gz
cd ocyd_*_linux_amd64
./install.sh
Из исходников:
git clone https://github.com/Dilikel/ocyd.git
cd 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.
Спасибо, что дочитали!

Комментарии 0