
Привет, Хабр! Когда делал ремонт у меня возникла идея сделать систему управления потоками аудио-видео между источниками и устройствами воспроизведения, чтобы, когда жена выгоняет из комнаты, нажать одну кнопку на телефоне и продолжить смотреть суточный марафон 24 часа Ле Мана в другой комнате. И чтобы всё работало с мобильного устройства.
Исходные данные: есть кабельное телевидение, сервер для хранения видео, игровая приставка, ПК и несколько ТВ в разных комнатах, на которых все это хочется запускать. Если с видео сервером проблем нет — на нем развернут DLNA-сервер, то все остальное будет требовать ручного коммутирования между источниками сигналов и ТВ (вариант с пучками HDMI-кабелей между комнатами не рассматривался из-за переизбыточности и сложности монтажа).
Принципиальная схема работы
В центре всей системы управления должен быть HDMI-матричный коммутатор (коммутирует любой вход на любой выход) с возможностью подключения к нему управляющего оборудования, например, через последовательный порт. Помимо него нужно само управляющее оборудование с соответствующим ПО и клиентские устройства для удаленного управления. Итоговая схема выглядит примерно так:

Поискав оборудование в интернетах, нашёл самый дешёвый подходящий вариант 4x4 коммутатора с поддержкой 4k 60fps и управлением по RS-232 на Aliexpress (впрочем, не удивительно, что именно там).
Через несколько недель коммутатор пришёл в пункт доставки, и я, радостный, принялся за его тестирование. Открыв коробку, увидел сам коммутатор, блок питания, инструкцию, пульт ДУ и антенну для сигнала пульта ДУ, которая подключается примерно метровым проводом, что позволяет разместить её в удобном доступном месте.
Однако, при условии работы в нескольких комнатах, пульт становится бесполезным.
Включив коммутатор в розетку, я подумал, что где-то стартует двигатель самолета —относительно небольшой коммутатор гудит как сервер, который пару лет не пылесосили (в дальнейшем удалось существенно понизить шум заменой вентиляторов).
Но, стоит отдать должное, подключив источники ввода-вывода, выяснилось, что работает он именно так, как я и ожидал: мгновенно переключение источников; никаких лагов на 4k 60fps не заметил, правда, напрячь все 4 источника и 4 выходных устройства технической возможности не было, но на 2x2 сигналах полет шикарный.
Разбираемся с последовательным портом
Следующий этап проверки — управление коммутацией через RS-232.
И тут начинается веселье. Инструкция представляет собой пару страничек 5 на 5 см с изображением как правильно вставить HDMI кабель (как будто есть несколько вариантов) и как подключить питание.
Подождите, но есть же последовательный порт, как мне его использовать? После поиска в гугле и общения с DeepSeek, пришлось начать общение с продавцом на площадке маркетплейса. На мой вопрос об использовании RS-232 последовал ответ (сопровождаемый традиционным «Hello, my dear friend!», которое сложно читать без акцента в голове): вот вам заводское java-приложение (странно, что про него не было написано в инструкции или хотя бы карточке товара), используйте его, по-другому использовать RS-232 не получится.
В целом, странно, ведь нет никакой разницы, что подает сигнал коммутатору, главное — правильно его сформировать, но по ответу было понятно, что продавец этого не понимает и пытаться получить от него какую-то дополнительную информацию бесполезно.
Что ж, настало время примерять на себя роль реверс-инженера.
Декомпиллировав присланный jar-файл и потратив некоторое время на изучение полученных файлов, были найдены следующие компоненты, вызывающие переключения сигналов:
private JComboBox jComboBox1;
private JComboBox jComboBox2;
private JComboBox jComboBox3;
private JComboBox jComboBox4;
private JComboBox jComboBox5;
private JComboBox jComboBox6;
private JComboBox jComboBox7;
private JComboBox jComboBox8;
Для каждого combobox’а задается свойство Name:
this.jComboBox1.setModel(new DefaultComboBoxModel(new String[] {
"Input 1", "Input 2", "Input 3", "Input 4",
"Input 5", "Input 6", "Input 7", "Input 8"
}));
this.jComboBox1.setName("1");
this.jComboBox1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
MX0808_MF.this.jComboBox1ActionPerformed(evt);
}
});
И обработчик выбора пункта выпадающего списка:
private void jComboBox1ActionPerformed(ActionEvent evt) {
JComboBox jcb = (JComboBox)evt.getSource();
this.selectPort(jcb);
}
На основании этого свойства и выбранного значения в combobox’е формируется сигнал для переключения:
public void selectPort(JComboBox jcb) {
if (!this.isBackInfo) {
int select = jcb.getSelectedIndex();
String name = jcb.getName();
String hex = Integer.toHexString(select);
msgSend("cir " + name + hex);
} else {
this.isBackInfo = false;
}
}
Сама отправка сигналов в выбранный порт со скоростью передачи 9600 бод:
public static void msgSend(String msg) {
buf.delete(0, buf.length());
data = (msg + "\r\n").getBytes();
(new Thread() {
public void run() {
try {
if (MX0808_HDMI_4K.mark) {
MX0808_HDMI_4K.outputStream.write(MX0808_HDMI_4K.data);
} else {
JOptionPane.showMessageDialog((Component)null, "The port is closed",
"Connect error", 0);
}
} catch (IOException var2) {}
}
}).start();
}
Сложно сказать, насколько декомпилированный код совпадает с исходным, так что оставим китайский код без комментариев.
Если систематизировать данные по всем combobox’ам, сигнал для переключения формируется из префикса «cir », а далее 2-х шестнадцатиричных цифр, обозначающих совместно входной и выходной порт, и символ конца строки.
По-видимому, плата коммутатора предназначается для коммутаторов до 8 входных и выходных портов, т.к. номер портов зашивается по правилу 8*(outputPort-1)+(inputPort-1).
Найти команду для получения текущего состояния портов оказалось проще — нужная строка «bc \r\n» сразу бросается в глаза, поскольку зашита в код константой. Эта команда вернет 8 строк состояний для каждого из портов.
Пишем свой сервер для управления
В принципе, этой информации достаточно для того, чтобы приступить к разработке своего ПО. Для реализации моей задумки нужно 2 компонента: сервер, к которому будет подключен коммутатор и который будет формировать управляющие сигналы в зависимости от запросов, пришедших с клиентов, и непосредственно клиент, запущенный на мобильном устройстве и посылающий соответствующие запросы.
Т.к. сервер минималистичен до безобразия (всего 4 ручки: signIn, signOut для того, чтобы абы кто не мог управлять моим коммутатором; getInfo для получения текущего состояния устройства; setHdmi для переключения некоторого output на input), реализовал его на Flask, сам сервер приводить не буду, поскольку в нем ничего интересного, только модуль для взаимодействия с коммутатором:
import serial
def switch_port(port, output, input):
serialPort = serial.Serial(port, 9600)
command = 'cir '+'{:02x}'.format(8*(output-1)+(input-1))+'\r\n'
serialPort.write(bytes(command, 'utf-8'))
serialPort.close()
def get_state(port):
serialPort = serial.Serial(port, 9600)
command = 'bc \r\n'
serialPort.write(bytes(command, 'utf-8'))
state = []
state.append(int(serialPort.readline().decode('utf-8')[2]) + 1)
state.append(int(serialPort.readline().decode('utf-8')[2]) + 1)
state.append(int(serialPort.readline().decode('utf-8')[2]) + 1)
state.append(int(serialPort.readline().decode('utf-8')[2]) + 1)
return state
Думаю, в этом коде сможет разобраться и школьник, при том, что он гораздо лаконичнее «китайского» декомпиллированного варианта.
Сервер должен работать 24/7, так что для того, чтобы развернуть его, я решил использовать свою RaspberyPi. Тут я столкнулся еще с одним непредвиденным осложнением: так как разворачивал сервер в докере, а с докером на RaspberyPi ранее не работал, выяснил, что собрать образ на ПК и закинуть на малинку так просто не получится из-за другой архитектуры процессора — кто хочет запускать контейнеры на малинке, должны и собрать их на малинке. К счастью, пересобрать образ для нужной платформы получилось менее чем за 10 минут: закинул исходники, «docker build» — и рабочий образ для RaspberyPi образ готов!
Развернув сервер, можно потыкать его через Postman — о чудо, оно работает, и работает как надо!
И последнее — клиент
При наличии какой-либо системы умного дома, например, Home Assistant, можно было бы интегрировать сервер в него и управлять всем оборудованием централизованно, но, в настоящий момент мои руки так и не добрались до глобальной автоматизации жилища, так что решил написать отдельное мобильное приложение для взаимодействия с сервером.
Так как приложение надо поставить всем членам семьи, нужно реализовать его максимально безболезненно и для Android и для IPhone, мой выбор пал на ReactNative (проверить на IPhone, к сожалению, так и не представилась возможность).
В приложении реализовано 2 окна. На первом — текущее состояние подключенного коммутатора, и возможность выбора для любого устройства отображения источник сигнала. Второе окно — настройки, где можно указать адрес сервера, учётную запись для подключения, а также псевдонимы для всех источников сигналов и устройств воспроизведения для удобной навигации в основном окне.
Все введенные настройки сохраняются в AsyncStorage, в запущенном виде приложение каждые 5 секунд обновляет состояние коммутатора (все-таки, его все еще можно переключать вручную) и в зависимости от ответа выводит состояние сервера (Online/Offline). Дизайн минималистичен, но большего и не надо:

Заключение
В итоге получилась готовая система для управления аудио-видео в доме. Выглядит это чудо примерно так:

Возможно, кто-то скажет, что задача надумана и едва ли кому-то потребуется часто перекоммутировать источники — да, действительно, пользуюсь я ей не так часто, как мне виделось на этапе создания, но приятно, когда ты смотришь суточный марафон 24 часа Ле Мана, и жена в ночи выгоняет в другую комнату, просто нажать одну кнопку в приложении, чтобы картинка появилась на экране другого телевизора. И именно это делает тебя счастливым в тот момент — путь, который ты преодолел и мысль, что ты смог его пройти)
Также подписывайтесь на Телеграм-канал Alfa Digital, где рассказывают о работе в IT и Digital: новости, события, вакансии, полезные советы и мемы.