Долгие годы я воспринимал систему 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 — логические адреса 0x40x80xB (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 только для записи.

Более удачным решением будет:

  1. Запустить один длительный процесс cec-client.6

  2. Сделать так, чтобы он выводил каждую строку TRAFFIC, которую мы будем парсить.

  3. Подавать ему на 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, или телевизор постоянно переключается обратно на свои динамики. Процесс в этих случаях будет тот же:

  1. Взять Pi с HDMI-портом. Подключите Pi к HDMI-входу ресивера или телевизора с помощью кабеля micro-HDMI — HDMI или переходника. Расположите его в надёжном месте.

  2. Снять данные с шины. Выполните echo "scan" | cec-client -s -d 1, чтобы убедиться, что ваша малинка видит все предполагаемые устройства, а также узнать их логические/физические адреса и роли.

  3. Зафиксировать «успешный» сценарий работы и «неудачный». Используйте cec-client -m -d 8, чтобы логировать трафик в процессе:

    1. Активации «успешного» пути (например, Apple TV исправно подключает звук 5.1).

    2. Активации «неудачного» пути (например, DTS возвращается к стерео-режиму, или ARC возвращается к динамикам ТВ).

  4. Сравнить трейсы трафика. Ищите опкоды, которые фигурируют в успешном трейсе, но отсутствуют в неудачном. В моём случае интересным отличием было присутствие сигнала 5f:72:01 после активации Apple TV и отсутствие чего-либо подобного при активации только приставки.

  5. Вставить опкод вручную. Зайдите на cec-o-matic.com и создайте там недостающий пакет7, после чего выполните cec-client -d 8.

    Эта команда для использования cec-client в интерактивном режиме. Затем введите tx ... для отправки нужного магического пакета и смотрите, приведёт ли это к каким-либо изменениям. Если нет, попробуйте ещё раз с другим пакетом.

    Скорее всего, вам нужно будет начать с пакета вроде 1f:... (от Recording 1 0x1 (логический адрес Pi) к Broadcast 0xF) или 15... (от Pi к Audio System 0x5) — это уже будет зависеть от ваших целей.

  6. Обернуть всё в код. Когда вы разберётесь, какой магический пакет вам нужен, заверните его в небольшую программу вроде той, что показана выше, и пусть Pi тихо делает своё дело на шине.

Наглядно представить успешный и неудачный пути можно так:

Здесь ваша задача найти недостающий шаг и научить Pi его выполнять.

На чём я остановился в своей конфигурации

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

Но есть ещё пара моментов, которые я не проработал:

  • Когда приставка уходит в сон, ТВ иногда «услужливо» переключается на антенный вход. У меня и антенны-то нет, поэтому в итоге вместо отката к Apple TV или известному рабочему входу возникает экран с надписью «No Signal». Такой ход технически корректен с позиции ТВ (его собственный тюнер всегда является рабочим источником), но ошибочен с точки зрения реального использования всего сетапа.

  • Мой механизм автоматического включения ТВ при закате иногда натыкается на нерабочий вход. Я настроил сценарий HomeKit, в котором ТВ включается примерно с заходом Солнца. Чаще всего это означает, что Apple TV пробуждается с приятной атмосферной заставкой. Но если последним источником до этого была приставка, ТВ включается на этом порте HDMI и просто показывает «No Signal», сбивая с толку гостей или домочадцев.

Это похожие проблемы, но они требуют чуть других решений:

  1. При переходе приставки в режим ожидания ТВ становится активным источником. Когда приставка уходит в сон, она освобождает шину, и ТВ услужливо переключается на свой тюнер. Вспомогательный скрипт мог бы отслеживать эту конкретную пару пакетов (console Standby, TV Active Source) и после короткого ожидания переключать источник на Apple TV.

  2. Отсутствие активного источника при автоматическом включении во время заката. В этом случае ТВ включается, но ни одно устройство (даже сам ТВ) не объявляет себя Active Source. В итоге он остаётся на последнем активном HDMI-порту, показывая «No Signal». Скрипт должен обнаруживать состояние, когда в течение N мс ТВ включён, Denon спит, а Active Source отсутствует, после чего будить Apple TV и ресивер, сменяя источник сигнала.

Или можно объединить оба этих решения, реализовав конечный автомат, который будет отслеживать «какой источник был активен последним» и автоматически откатываться к Apple TV, если на шине тишина, или ТВ переключается на свою антенну. В любом случае задача обеспечения адекватного вывода будет лежать на Pi.

Это уже превратит Pi в более общего «смотрителя HDMI», который будет не просто обеспечивать привязку ARC к ресиверу при воспроизведении аудио с внешних источников, но и откатывать систему обратно, когда с этих источников сигнала нет.

Похоже, дело попахивает развитием небольшого кустарного производства двухстраничных скриптов. Если вы адаптируете мой приём под решение каких-то своих траблов с HDMI-CEC, пришлите трейсы пакетов — люблю собирать фольклор.

Сноски

  1. Логические адреса CEC обычно записываются в шестнадцатеричном виде, поэтому десятичный адрес 11 = 0xB = Playback 3. Дополнительно всё запутывается тем, что cec-client использует для логических адресов шестнадцатеричную запись, но обрезает префикс 0x. В этой статье для большей ясности я префиксы 0x добавил. ↩︎ ↩︎

  2. Полная неразбериха. Здесь мне реально помогло https://www.cec-o-matic.com . ↩︎

  3. Как ни удивительно, HDMI-CEC определяет лишь три логических адреса для устройств воспроизведения0x4 (Playback 1), 0x8 (Playback 2) и 0xB (Playback 3). Это нормально, если у вас одна ТВ-приставка и пара игровых. У меня же к Denon было подключено четыре устройства воспроизведения (Apple TV, PS5, Switch 2, Xbox). Согласно HDMI-CEC, только три из них могут являться «реальными» устройствами воспроизведения одновременно. Поэтому, когда активируется четвёртое, ТВ и AVR приходится на ходу перераспределять логические адреса. На практике это выглядело так: если Switch была включена, изменить источник на Xbox было невозможно — моргал чёрный экран, и источником обратно выбирался Switch. Чертовщина какая-то. Но так как я использую лишь одну приставку одновременно, эта проблема меня не напрягает, и заморачиваться исправлением, пожалуй, нет смысла. ↩︎ ↩︎ 

  4. Не новая версия 8K, а старая, которую я пытался заставить работать с моей конфигурацией 4K120↩︎

  5. Флаг -d — это битовая маска для уровней логирования (ERROR=1WARNING=2NOTICE=4TRAFFIC=8DEBUG=16ALL=31), поэтому установка, например, -d 4 может не показать ничего интересного; истина кроется в исходном коде libcec. Так что -d 8 означает «только трафик». Изначально я попробовал -d 4 (notice) и при включении Apple TV ничего не увидел. В результате пришлось лезть в исходники libcec. Документация этой библиотеки оказалась на удивление скудной, и во многих источниках о ней содержалась обманчивая информация. ↩︎

  6. Для libCEC есть привязки Python вроде python-cec, но документация для них никакая, и мне оказалось проще обернуть cec-client как подпроцесс. ↩︎

  7. Использование ИИ-инструментов здесь тоже уместно — вставьте успешный и неудачный трейсы в тот же ChatGPT и попросите найти отличия, указав, что среди них могут быть недостающие пакеты CEC. Для более качественного анализа советую использовать модель с хорошими рассуждающими способностями, например, GPT-5.1-Thinking. ↩︎

Комментарии (11)


  1. kuznet1
    19.12.2025 20:41

    Вроде ничего особенного, но почему-то статья воспринимается как текст на очень богатом