golang

Как я написал на Go демон для экрана кулера Ocypus в Linux

  • воскресенье, 15 ноября 2026 г. в 00:00:16
https://habr.com/ru/articles/1107776/

Всем привет!

Моя основная система — Linux (Arch). Мне она очень нравится, но относительно недавно я обновил процессор и решил заодно поменять кулер. Выбор пал на Ocypus Delta A62 EX Digital. У него есть простая, но симпатичная фича: небольшой экранчик, на котором можно показывать температуру процессора или видеокарты.

У Ocypus есть официальное приложение для Windows, а для Linux — нет. Поэтому я решил написать своё. В то время я как раз изучал Go, и он почти идеально подходит под такую задачу, но, честно говоря, это было ещё и желанием потренироваться в языке. К слову, я думаю когда‑нибудь переписать всё на C или Rust, но из‑за большого объёма учёбы времени на это пока нет.

Так и начался проект ocyd.

Как устроен путь от идеи до рабочего демона

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 < 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

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.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.

install.sh:

  1. ищет готовые бинарники: сначала рядом со скриптом (если вы скачали архив релиза), затем в bin/ (если склонировали репозиторий). Если их нет, соберёт сам;

  2. спрашивает, оставить ли VID:PID по умолчанию (1a2c:434d, Delta A62 EX Digital) или ввести свои;

  3. копирует бинарники в /usr/bin, а udev‑правило в /etc/udev/rules.d;

  4. создаёт конфиг, если его ещё нет;

  5. спрашивает, нужен ли 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 (или вы просто нашли у меня ошибку), буду рад любой обратной связи:

  1. узнайте VID:PID командой lsusb;

  2. поменяйте их в конфиге и в udev‑правиле (или просто запустите ./install.sh и введите свои значения);

  3. напишите в issue, даже если не заработало: укажите точную модель, дистрибутив, VID:PID и логи journalctl --user -u ocyd.service.

Также буду благодарен за code review: если вы видите, как сделать лучше, пишите в комментариях или в issue.

Спасибо, что дочитали!