
Долгие годы я воспринимал систему HDMI-CEC как домового: иногда полезная, часто непредсказуемая и всегда загадочная. У меня в гостиной собран несложный мультимедиа-центр — ТВ Samsung с поддержкой ARC (не eARC, которая заслуживает отдельного поста), Denon AVR-X1700H, спрятанный в кладовке, Apple TV, несколько подключённых к Denon игровых приставок и Raspberry Pi 4, управляющий системой Homebridge. Что касается CEC, то в Apple TV эта фича работает прекрасно, но вот приставки ведут себя так, будто едва с ней знакомы. Они будят ТВ, переключают источник, но оставляют Denon в режим ожидания, вынуждая меня переключать вывод аудио вручную.

Я уже описывал свою медиа-сборку, которую организовал у себя в кладовке. Так что, кому интересно узнать всю схему подключения (и посмотреть фото до/после), рекомендую начать с той статьи.
С учётом уже организованной медиа-кладовки переподключать всё к телевизору явно было не вариант, а отключать CEC нецелесообразно (Apple TV исправно работает и используется чаще всего). Первыми мне на ум пришли традиционные системы автоматизации — прописать сценарии HomeKit, чтобы включение ТВ вело к включению ресивера, либо использовать активацию устройств на основе потребляемой энергии через умную розетку Eve Energy. В целом, такая схема работала, но каждый дополнительный этап вносил 30 секунд задержки, а то и более. Последней попыткой в этой эпопее стал плагин homebridge-cec-tv-control, но из прочтения документации я понял, что сигналы CEC в итоге будут идти к ресиверу через Node, Homebridge и HomeKit. А поскольку у меня в систему уже подключён Pi, то пропуск этих слоёв и прямая передача через /dev/cec0 была явно более быстрым путём.
Попотев над этой задачей несколько вечеров, я разместил Pi на шине HDMI, откуда он следит за поступлением сигналов с приставок и отправляет единственную команду, которой Samsung и Denon должны были обмениваться сами.
В этой статье я изложил процесс в соответствии с этапами его реализации — моделирование в уме лаконичной схемы CEC, организация мониторинга шины, копирование действий Apple TV, их обёртывание в код Python и отправка в виде службы systemd.
Немного вводных по HDMI-CEC
Система High-Definition Multimedia Interface Consumer Electronics Control, чаще известная как HDMI-CEC, представляет собой сторонний канал передачи данных, пролегающий параллельно HDMI-каналу аудио/видео. Все подключённые к шине устройства общаются на языке логических адресов (0x0 для телевизора, 0x5 для аудиосистем, 0x4/0x8/0xB1 для устройств воспроизведения и так далее) и крохотных опкодов2 вроде 0x82 (Active Source) или 0x72 (Set System Audio Mode). Физические адреса в этой топологии представляют своеобразную «широту/долготу», то есть 3.0.0.0 может означать «Подключение к AVR через HDMI 3».
По идее, CEC должна облегчать для пользователя управление электроникой, поэтому в здравой системе процесс разворачивается так: приставка выходит из сна и сообщает об этом, ТВ замечает появление устройства с поддержкой ARC, некий девайс отправляет сигнал «используй аудиосистему», активируется ресивер, и звук начинает идти через большие колонки. В моём случае такой алгоритм работал, только когда в схеме присутствовал Apple TV. Если же активировалась приставка, ТВ переключал источник, но звук всё так же продолжал идти через крохотные динамики телевизора.
Озадачившись исправлением этой неразберихи, я сначала записал идентификаторы, которыми каждое устройство представляло себя на шине. Вот конкретные роли CEC в моей системе домашнего кинотеатра:
TV — логический адрес
0x0.Audio system (Denon AVR-X1700H) — логический адрес
0x5.Playback devices — логические адреса
0x4,0x8,0xB(Apple TV, PS5, Switch 2 и Xbox — все соперничают за три слота воспроизведения3).Broadcast — логический адрес
0xF(передача сообщения всем устройствам на шине).
А вот основные опкоды, которые меня интересуют:
0x82— Active Source («Теперь я активный источник ввода»).0x84— Report Physical Address («Моё место в HDMI-дереве»).0x70— System Audio Mode Request.0x72— Set System Audio Mode (Сигнал Denon «Теперь системное аудио на мне»).
Мониторинг шины CEC с помощью cec-client
Порт micro-HDMI на моей малинке поддерживает интерфейс /dev/cec0, и если соединить её с ресивером простым кабелем HDMI — micro-HDMI, то можно мониторить CEC-трафик от всех подключённых к этому ресиверу устройств.

Имея печальный опыт с Hue Play Sync Box4, поначалу я сомневался: включение в линию перед ТВ любого HDMI-сплиттера или гаджета обычно вело к странным сбоям EDID, проблемам с согласованием HDR или полной потере сигнала. Но когда я понял, что Pi никак не вмешивается в HDMI-хэндшейк, то мои сомнения отпали. После подключения к HDMI-входу ресивера он работает так же, как и любой другой участник общей шины CEC. Никакой регенерации сигнала, никакого искажения EDID, ничего, что могли бы заметить другие устройства цепочки.
В итоге получилась такая схема подключения:

Теперь можно установить cec-client из libcec:
sudo apt update
sudo apt install cec-utils
Затем выполнить сканирование, чтобы проверить, какие устройства отзовутся:
echo "scan" | cec-client -s
Ниже показан пример результатов сканирования моей конфигурации. Как видите, и Xbox, и Switch3 заняли один логический адрес 0x81:
CEC bus information
===================
device #0: TV
address: 0.0.0.0
active source: no
vendor: Samsung
osd string: TV
CEC version: 1.4
power status: on
language: eng
device #1: Recorder 1
address: 3.3.0.0
active source: no
vendor: Pulse Eight
osd string: CECTester
CEC version: 1.4
power status: on
language: eng
device #4: Playback 1
address: 3.1.0.0
active source: yes
vendor: Unknown
osd string: Switch 2
CEC version: 1.3a
power status: on
language: ???
device #5: Audio
address: 3.0.0.0
active source: no
vendor: Denon
osd string: AVR-X1700H
CEC version: 1.4
power status: on
language: ???
device #8: Playback 2
address: 3.2.0.0
active source: no
vendor: Apple
osd string: Apple TV
CEC version: 2.0
power status: standby
language: ???
device #B: Playback 3
address: 3.6.0.0
active source: no
vendor: Sony
osd string: PlayStation 5
CEC version: 1.3a
power status: standby
language: ???
Если все ожидаемые устройства обозначились, используйте режим мониторинга с подходящим уровнем5 логирования:
cec-client -m -d 8
Эта команда ведёт к отслеживанию всех транзакций по шине без вмешательства со стороны Pi.
Строка TRAFFIC: [...] >> bf:82:36:00 означает, что логический адрес 0xB (PS5) передаёт по шине сигнал Active Source (0x82) с физическим адресом 3.6.0.0. Именно такой пакет должна отправлять любая приставка при пробуждении.
Разбор магического хэндшейка
Итак, я включаю систему в режим ожидания, начинаю логирование, потом бужу Apple TV. Pi зарегистрировал ожидаемый сигнал Active Source, следом за которым Denon сообщил о том, что взял на себя воспроизведение аудио:
>> 8f:82:32:00 # Apple TV (logical 8) -> Broadcast: Active Source
...
>> 8f:a6:06:10:56:10 # Apple TV (logical 8) -> Broadcast: ???
>> 5f:72:01 # Denon (logical 5) -> Broadcast: Set System Audio Mode (on)
По-русски:
Apple TV объявляет себя активным источником.
Apple TV передаёт некие магические биты?
Вскоре после этого Denon сообщает всем, что «System Audio Mode включён», и TV без вопросов сохраняет в качестве устройства воспроизведения ресивер, не переключаясь обратно на динамики телевизора.
Ровно такой же эксперимент я проделал с PS5, Xbox и Switch 2, но получил уже другой результат:
>> bf:82:36:00 # PS5: Active Source
# …много всяких сообщений, но нет 5f:72:01
Так что же это был за пакет 8f:a6:06:10:56:10, когда подключался Apple TV? В журнале отладки cec-client показывает UNKNOWN (A6). Подозреваю, что libCEC маркирует его как UNKNOWN, потому что этот пакет относится к диапазону конкретного вендора. Байты 06:10:56:10 могут нести проприетарное содержимое Apple, например, некую функцию или расширенную команду управления. Возможно, между Samsung и Apple здесь есть соглашение, которое и ведёт к правильной реакции Denon. Интересно, но я не могу полагаться на этот довод, так как ему нет документального подтверждения, и ручная отправка этого сигнала с логического адреса малинки не сработала. Имитировать Apple TV по CEC нецелесообразно и наверняка опасно.
Однако с помощью cec-o-matic.com оказалось несложно создать пакет CEC с типичным запросом на включение системного аудио, чтобы передавать этот пакет с помощью Pi:
15:70:00:00 # TV (1) -> Audio (5): System Audio Mode Request
Пояснение:
15= источник0x1(Recorder 1 = Pi) отправляет сигнал получателю0x5(Audio System = Denon).70= опкод System Audio Mode Request.00:00= операнды (физический адрес TV, 0.0.0.0, плюс «статус системного аудио» = off/0, которые Denon понимает как «согласуй режим системного аудио и включи ARC»).
Как только я ввёл этот запрос в интерактивную оболочку cec-client с помощью tx 15:70:00:00, Denon включился, и ARC закрепился за ним даже при том, что включены были только приставка и ТВ. Проверив вывод звука с телевизора, я убедился, что так и есть:

Начало вырисовываться решение проблемы — когда приставка просыпается и отправляет сигнал Active Source, в процесс должен включаться Pi и отправлять ресиверу запрос 15:70:00:00 для инициации согласования передачи аудио.
Не засоряйте шину!
Теперь, когда основа процесса автоматизации стала ясна, самым очевидным действием казалось написание скрипта Bash, который будет запускать cec-client каждые несколько секунд и отправлять on 5. Такой подход, конечно, сработает, но не идеально. Поясню:
Использование цикла означает, что автоматизация происходит с задержкой, а не в виде реагирования на события шины CEC.
В каждой итерации мы запускаем новый
cec-client, привязываемся к /dev/cec0, отправляем одну команду и завершаем процесс.CEC же является общей шиной, а не каналом GPIO только для записи.
Более удачным решением будет:
Запустить один длительный процесс
cec-client.6Сделать так, чтобы он выводил каждую строку
TRAFFIC, которую мы будем парсить.Подавать ему на stdin команды
tx..., только когда нужно вмешаться.
Единственная проблема в том, что в режиме мониторинга (-m) нельзя передавать команды. Поэтому для автоматизации переключаемся в:
cec-client -d 8
Директивы -m здесь уже нет. сec-client по-прежнему выводит весь трафик, но теперь также принимает команды. Наш скрипт Python использует его как мост между HDMI-средой и нашей логикой, в которой stdout отражает поток событий, а stdin выступает каналом управления.
Python-скрипт
Без экспериментов не обошлось, но в целом было нетрудно написать миниатюрный скрипт для отслеживания активации приставок и отправки в нужный момент магической команды 15:70:00:00. Исходники я положил на GitHub: jlian/cec_auto_audio
Вот его логика:
Запуск
cec-client -d 8в виде подпроцесса.Парсинг строк
TRAFFIC.Отслеживание поступления сигналов Active Source (
0x82) с любого логического адреса Playback (0x4/0x8/0xB).Отслеживание, когда Denon в последний раз отправлял сигнал Set System Audio Mode (
5f:72:01), чтобы не вмешиваться в работу собственной логики Apple TV или телевизора.Отправка
tx 15:70:00:00, если этого ещё не сделало никакое другое устройство, и не более одного раза при каждом пробуждении приставки.
Несколько комментариев:
В скрипте не прописаны никакие имена устройств, производители или физические адреса.
Любой логический адрес Playback (
0x4/0x8/0xB) он преобразует в Active Source в виде события «пробуждения приставки».Когда Apple TV / Samsung / Denon справляются сами, он бездействует (так как поступает реальный сигнал
5f:72:01).Выполняется скрипт как одиночный длительный процесс, привязанный к одному экземпляру
cec-client.
Чтобы скрипт запускался при загрузке и продолжал работать, я завернул его под вид простой службы systemd. Использованный для этого юнит-файл можно найти в разделе README на GitHub. Пользуюсь этим решением уже несколько дней — работает безупречно.
Обобщение всего проделанного
Надеюсь, что этой статьи будет достаточно, чтобы вы могли адаптировать изложенный подход под решение своих проблем с CEC. Моё решение ориентировано конкретно на сценарий связки «Denon + Samsung + приставки», но тот же принцип должен сработать и в отношении других причуд CEC.
Возможно, вы испытываете проблемы не с подключением приставок, а с активацией AVR. Может, у вас не согласуется DTS, или телевизор постоянно переключается обратно на свои динамики. Процесс в этих случаях будет тот же:
Взять Pi с HDMI-портом. Подключите Pi к HDMI-входу ресивера или телевизора с помощью кабеля micro-HDMI — HDMI или переходника. Расположите его в надёжном месте.
Снять данные с шины. Выполните echo
"scan" | cec-client -s -d 1, чтобы убедиться, что ваша малинка видит все предполагаемые устройства, а также узнать их логические/физические адреса и роли.-
Зафиксировать «успешный» сценарий работы и «неудачный». Используйте
cec-client -m -d 8, чтобы логировать трафик в процессе:Активации «успешного» пути (например, Apple TV исправно подключает звук 5.1).
Активации «неудачного» пути (например, DTS возвращается к стерео-режиму, или ARC возвращается к динамикам ТВ).
-
Сравнить трейсы трафика. Ищите опкоды, которые фигурируют в успешном трейсе, но отсутствуют в неудачном. В моём случае интересным отличием было присутствие сигнала
5f:72:01после активации Apple TV и отсутствие чего-либо подобного при активации только приставки. -
Вставить опкод вручную. Зайдите на cec-o-matic.com и создайте там недостающий пакет7, после чего выполните
cec-client -d 8.Эта команда для использования
cec-clientв интерактивном режиме. Затем введитеtx ...для отправки нужного магического пакета и смотрите, приведёт ли это к каким-либо изменениям. Если нет, попробуйте ещё раз с другим пакетом.Скорее всего, вам нужно будет начать с пакета вроде
1f:...(от Recording 10x1(логический адрес Pi) к Broadcast0xF) или15...(от Pi к Audio System0x5) — это уже будет зависеть от ваших целей. Обернуть всё в код. Когда вы разберётесь, какой магический пакет вам нужен, заверните его в небольшую программу вроде той, что показана выше, и пусть Pi тихо делает своё дело на шине.
Наглядно представить успешный и неудачный пути можно так:

Здесь ваша задача найти недостающий шаг и научить Pi его выполнять.
На чём я остановился в своей конфигурации
Apple TV продолжает исправно работать. PS5, Xbox и Switch теперь будят телевизор, Pi в течение полусекунды отправляет сигнал Denon, и вывод аудио остаётся закреплённым за ресивером. Задержка получается незначительная и воспринимается как естественная. Малинку я расположил там же в кладовке, где она играет роль заумного пульта управления.

Но есть ещё пара моментов, которые я не проработал:
Когда приставка уходит в сон, ТВ иногда «услужливо» переключается на антенный вход. У меня и антенны-то нет, поэтому в итоге вместо отката к Apple TV или известному рабочему входу возникает экран с надписью «No Signal». Такой ход технически корректен с позиции ТВ (его собственный тюнер всегда является рабочим источником), но ошибочен с точки зрения реального использования всего сетапа.
Мой механизм автоматического включения ТВ при закате иногда натыкается на нерабочий вход. Я настроил сценарий HomeKit, в котором ТВ включается примерно с заходом Солнца. Чаще всего это означает, что Apple TV пробуждается с приятной атмосферной заставкой. Но если последним источником до этого была приставка, ТВ включается на этом порте HDMI и просто показывает «No Signal», сбивая с толку гостей или домочадцев.
Это похожие проблемы, но они требуют чуть других решений:
При переходе приставки в режим ожидания ТВ становится активным источником. Когда приставка уходит в сон, она освобождает шину, и ТВ услужливо переключается на свой тюнер. Вспомогательный скрипт мог бы отслеживать эту конкретную пару пакетов (console Standby, TV Active Source) и после короткого ожидания переключать источник на Apple TV.
Отсутствие активного источника при автоматическом включении во время заката. В этом случае ТВ включается, но ни одно устройство (даже сам ТВ) не объявляет себя Active Source. В итоге он остаётся на последнем активном HDMI-порту, показывая «No Signal». Скрипт должен обнаруживать состояние, когда в течение N мс ТВ включён, Denon спит, а Active Source отсутствует, после чего будить Apple TV и ресивер, сменяя источник сигнала.
Или можно объединить оба этих решения, реализовав конечный автомат, который будет отслеживать «какой источник был активен последним» и автоматически откатываться к Apple TV, если на шине тишина, или ТВ переключается на свою антенну. В любом случае задача обеспечения адекватного вывода будет лежать на Pi.
Это уже превратит Pi в более общего «смотрителя HDMI», который будет не просто обеспечивать привязку ARC к ресиверу при воспроизведении аудио с внешних источников, но и откатывать систему обратно, когда с этих источников сигнала нет.
Похоже, дело попахивает развитием небольшого кустарного производства двухстраничных скриптов. Если вы адаптируете мой приём под решение каких-то своих траблов с HDMI-CEC, пришлите трейсы пакетов — люблю собирать фольклор.
Сноски
-
Логические адреса CEC обычно записываются в шестнадцатеричном виде, поэтому десятичный адрес
11=0xB=Playback 3. Дополнительно всё запутывается тем, чтоcec-clientиспользует для логических адресов шестнадцатеричную запись, но обрезает префикс0x. В этой статье для большей ясности я префиксы0xдобавил. ↩︎ ↩︎ -
Полная неразбериха. Здесь мне реально помогло https://www.cec-o-matic.com . ↩︎
-
Как ни удивительно, HDMI-CEC определяет лишь три логических адреса для устройств воспроизведения:
0x4(Playback 1),0x8(Playback 2) и0xB(Playback 3). Это нормально, если у вас одна ТВ-приставка и пара игровых. У меня же к Denon было подключено четыре устройства воспроизведения (Apple TV, PS5, Switch 2, Xbox). Согласно HDMI-CEC, только три из них могут являться «реальными» устройствами воспроизведения одновременно. Поэтому, когда активируется четвёртое, ТВ и AVR приходится на ходу перераспределять логические адреса. На практике это выглядело так: если Switch была включена, изменить источник на Xbox было невозможно — моргал чёрный экран, и источником обратно выбирался Switch. Чертовщина какая-то. Но так как я использую лишь одну приставку одновременно, эта проблема меня не напрягает, и заморачиваться исправлением, пожалуй, нет смысла. ↩︎ ↩︎ -
Не новая версия 8K, а старая, которую я пытался заставить работать с моей конфигурацией 4K120. ↩︎
-
Флаг
-d— это битовая маска для уровней логирования (ERROR=1,WARNING=2,NOTICE=4,TRAFFIC=8,DEBUG=16,ALL=31), поэтому установка, например,-d 4может не показать ничего интересного; истина кроется в исходном коде libcec. Так что-d 8означает «только трафик». Изначально я попробовал-d 4(notice) и при включении Apple TV ничего не увидел. В результате пришлось лезть в исходникиlibcec. Документация этой библиотеки оказалась на удивление скудной, и во многих источниках о ней содержалась обманчивая информация. ↩︎ -
Для
libCECесть привязки Python вроде python-cec, но документация для них никакая, и мне оказалось проще обернутьcec-clientкак подпроцесс. ↩︎ Использование ИИ-инструментов здесь тоже уместно — вставьте успешный и неудачный трейсы в тот же ChatGPT и попросите найти отличия, указав, что среди них могут быть недостающие пакеты CEC. Для более качественного анализа советую использовать модель с хорошими рассуждающими способностями, например, GPT-5.1-Thinking. ↩︎
kuznet1
Вроде ничего особенного, но почему-то статья воспринимается как текст на очень богатом