Покупая девайс, мы, в принципе, понимаем, что вечно он не прослужит: разъёмы износятся и/или сам прибор выйдет из моды. Но лично меня наиболее удручает ещё одна причина избавляться от техники: для неё просто перестают делать драйвера.

Успех USB особенно примечателен. Этот стандарт с нами уже очень долго, и разъём его практически не менялся (в основном, не считая USB-C). Это значит, что очень старые устройства под USB 1 по-прежнему можно применять в системах, продаваемых сегодня. Как минимум, так должно быть, если у старых устройств есть драйверы для тех операционных систем, что актуальны сегодня.

Универсальные классы USB для видеоаудио и хранения данных позволили сложиться стандарту, обязательному к реализации на устройствах, чтобы гарантировать, что эти устройства смогут функционировать после минимальной настройки драйверов или вообще без драйверов. Но всё равно сохранялось условие: устройства должны быть сделаны в период, когда эти стандарты существовали.

Знакомьтесь: QuickCam Express

На той неделе один мой хороший друг разгребал барахло и вручил мне логитековскую веб-камеру QuickCam Express. Признаться, тогда меня сильно накрыло ностальгией: ведь моя первая веб-камера была именно такой модели. Поэтому, неся её домой, я подумывал, что здорово было бы на рабочих созвонах ненадолго возродить «дух нулевых». 

Правда, складывалось впечатление, что драйверов для QuickCam Express не делалось со времён Windows XP. Подключил камеру к машине под Linux — никакого модуля не загрузилось. Тогда подключил к виртуальной машине с Windows 10, и та мне сообщила, что обнаружила неизвестное устройство. То есть с официальной поддержкой этого устройства у меня не срослось.

Что меня особенно раздосадовало, ведь я уже принёс эту штуку домой. Не желая так сразу сдаваться, я решил взять и проверить, работает ли эта веб-камера. Установил Windows XP: посмотрю, думаю, станет ли она корректно функционировать под операционной системой своего времени.

(Кстати, отмечу: устанавливать Windows XP на весьма быстром современном железе очень забавно. Сначала мастер установки сообщает, что до завершения процесса остаётся 30 минут, а затем влёт справляется со всей работой менее чем за 15 секунд)

После установки я скачал Windows Movie Maker, чтобы проверить, будет ли нормально работать веб-камера, и с большим удовольствием убедился: да, работает (должен сказать, что с 1999 года качество вебок определённо улучшилось).

Поэтому вопрос встал таким образом: как заставить эту веб-камеру работать на современной операционной системе, тогда как драйверы для неё существуют только под Windows XP?

Проводим время в пользовательском пространстве

В одной из предыдущих публикаций я рассказывал, как очень дёшево купил несколько устройств «VGA2USB» для видеосъёмки. Позже я понял, что они обошлись в такую цену именно потому, что современных драйверов под них не было. Поэтому я решил сам написать для них драйвер пользовательского пространства (и теперь гоняю их не реже чем раз в месяц!).

Драйвер пользовательского пространства встраивается в саму программу, а не ставится в операционную систему как модуль с кодом. Соответственно, такой драйвер может работать на разных версиях операционных систем, а то и на разных платформах, и для этого требуются лишь минимальные изменения кода.

Чтобы составить впечатление, что же делает драйвер Windows XP, я загрузил usbmon себе на ПК, где у меня на виртуальной машине работала Windows XP, а затем записал USB-трафик, которым обмениваются виртуальная машина и веб-камера. При реверс-инжиниринге такая практика бесценна, поскольку позволяет в режиме реального времени наблюдать стенограмму «определённо качественной коммуникации», на основе которой затем можно собирать собственный драйвер.

Я был настроен оптимистично, так как коммуникация казалась элементарной: сначала требовалось предзагрузить самые базовые настройки, а затем изохронно перенести информацию в поток данных, который тоже выглядел совсем безыскусно.

Далее я принялся искать, а не написал ли уже кто-нибудь драйвер для этой веб-камеры под Linux. Оказалось, что действительно такой код есть, в форме qc-usb. Отталкиваясь от него, я постарался соорудить самую базовую конфигурацию, которая обеспечила бы мне потоковую передачу визуальных данных.

Для начала я воспользовался go-usb, уже знакомой мне обёрткой для libusb, и взялся искать VID и PID для работы с QuickCam по usb:

ctx := gousb.NewContext()
defer ctx.Close()

// idVendor=046d, idProduct=0870,
dev, err := ctx.OpenDeviceWithVIDPID(0x046d, 0x0870)
if err != nil || dev == nil {
        log.Fatalf("Could not open a device: %v", err)
}


deviceConfig, err := dev.Config(1)
if err != nil {
        log.Fatalf("Failed to get USB config for device: %v", err)
        return
}

На данном этапе уже можно взять интерфейс для устройства, через который можно было бы наладить коммуникацию.

// Config.Interface(num, alt int)
USBInterface, err := deviceConfig.Interface(0, 0)
if err != nil {
        log.Fatalf("cannot grab control interface %v", err)
}

Под капотом для любого устройства, которое вы вставляете в USB-порт, предусматривается конфигурация, в которой указывается один или более интерфейсов, а внутри такого интерфейса наверняка есть несколько конечных точек. Несколько конечных точек зачастую требуется потому, что у USB-устройства не одна, а несколько функций. Например, на звуковой карте с USB может быть функция вывода, микрофон, а также кнопки для управления громкостью. Зачастую такие возможности реализуются через разные конечные точки и потенциально разные конфигурации интерфейса — в зависимости от того, в каком качестве управляющий драйвер соберётся их использовать.

Поскольку меня интересовало именно то, как получить с веб-камеры визуальные данные, я экспортировал из wireshark в формате CSV все управляющие пакеты, нужные для настройки веб-камеры. Эти пакеты VM отправила на вебку прежде, чем та дала изображение на вывод. Также я поставил эти пакеты в драйвер пользовательского пространства, чтобы их операции могли воспроизводиться (я всё ещё не вполне понимал, что делают эти пакеты).

Затем я перешёл к настройке передачи. Передачу я организовал через конечную точку, использующую изохронный поток данных (это тип обмена данными по USB, гарантирующий те показатели задержки и пропускной способности, что прописаны для этой шины). Оказалось, результат такой:

libusb: invalid param [code -2]

Меня это немного смутило, и я пошёл за разъяснениями в логи ядра, так как именно там обычно выводится информация, полезная в таких ситуациях:

kernel: usb 5-1.3: usbfs: usb_submit_urb returned -90

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

$ cat /usr/src/linux-headers-`uname -r`/include/uapi/asm-generic/errno.h | grep 90
#define        EMSGSIZE        90        /* Слишком длинное сообщение */

Как видим, 90 (или -90) означает «слишком длинное сообщение». Здесь я на некоторое время затупил, пока не решил вновь заглянуть в конфигурационные структуры USB-устройства…

Далее я заметил, что у нулевого интерфейса (который я выбрал просто по привычке) максимальный размер пакета (MaxPacketSize) равен 0, а я собирался через эту конечную точку передавать картинки.

Таким образом, все попытки получить данные с камеры через первый USB-интерфейс были обречены на провал. Конечно, уместен вопрос: а почему на первом интерфейсе у веб-камеры задан MaxPacketSize = 0? Кто знает… Но доступен и ещё один интерфейс, зеркальный для 0-го, но с MaxPacketSize = 1023. Довольно неплохо, и совсем скоро я наладил потоковую передачу данных из веб-камеры!

Теперь можно считывать данные с веб-камеры фрагментами по 1023 байта. Но нам ещё нужно декодировать этот поток данных, извлечь из него смысл!

Разбираемся с потоком

По-видимому, в веб-камере действует протокол сегментирования, позволяющий забирать данные изображений фрагментами по 1023 байта. Исходя из имеющихся паттернов, можем предположить, что на отметке 0x02,0x00 начинается новый кадр. Но я решил ради экономии времени заглянуть в драйвер qc-usb, и оказалось, что 0x02,0x00 — это ID сообщения для фрагмента изображения. Предполагается, что эти данные будут добавляться в более крупный буфер изображений, а следующие 16 бит означают длину фрагмента. Начало и конец каждого кадра кодируются разными ID сообщений. В любом случае, реализовать это очень легко, и достаточно скоро у меня были полные «покадровые» данные.

Теперь задача посложнее: нужно выяснить, как именно кодируются изображения. Сначала я попытался взять заявленное разрешение, которое сообщает Windows XP (320x240) и непосредственно отрисовать картинку в виде 24-битных цветных RGB-пикселов:

Что ж, попытка не пытка. На данном этапе я уже подозревал, что мы по-прежнему работаем с сырыми сенсорными данными, без применения какого-либо сжатия. Дело в том, что всякий раз приходила порция данных в 52096 байт. Если бы применялось сжатие, то они бы как-то варьировались (пусть и немного).

Но, чтобы это подтвердить, я просто поднёс к веб-камере ладонь и сделал ещё несколько снимков, после чего убедился, что яркость получавшейся мешанины хотя бы менялась именно в те моменты, когда я заслонял веб-камеру рукой.

Вот мы и выяснили, что, как минимум, рассматриваем сырые образцы, снятые сенсором. Теперь бы узнать, как упакованы эти значения, приходящие от сенсора?

Я исследовал сенсор веб-камеры и выяснил, что имею дело с Photobit PB-100. Разрешение этого сенсора составляет 352x288. Далее я предположил, что каждый пиксель должен содержать около 4 бит (если исходить из того, что размер кадра равен 52096 байт:

 (52096*8)/(352*288) = 4.11

А что, если попробовать такое новое разрешение и предположить, что значение каждого сенсора — это 4-разрядное число?

Тогда у меня получилась картинка, на которой я действительно на себя походил (у меня даже нашлась фотография, где я полностью накрыт одеялом – она очень пригодилась в качестве тестового паттерна!), но скомканная и без каких-либо цветов.

Занимаясь обработкой изображений, получаешь несомненный бонус: есть где насмотреться на «современное искусство» в стиле «обработка цифровых сигналов». Думаю, в тот раз у меня получилась работа в духе Энди Уорхола.

Затем я осознал, что мне хватает пикселей только на монохромное изображение, но, если я нарисую его неправильно, то получится «полноразмерная» картинка, весьма смахивающая на мой портрет!  

finalImg := image.NewRGBA(
        image.Rect(0, 0, 400, 400))
xLimit := 352

for idx, _ := range imgData {
        if idx+3 > len(imgData) {
                break
        }

        x := idx % xLimit
        y := idx / xLimit

        finalImg.SetRGBA(x, y, color.RGBA{
                imgData[idx],
                imgData[idx+1],
                imgData[idx+2],
                255,
        })
}

Дальше требовалось разобраться с цветами. Подумал, что тут меня ждут уже знакомые приключения, как в тот раз, когда я упражнялся с преобразованиями пространства цветов YUV (поскольку вариант с RGB явно не работал) — и безуспешно.

Так что я вновь принялся читать исходный код драйвера Linux qc-usb и обнаружил, что к сырым данным изображения применяется фильтр Байера! Так что мне предстояло самостоятельно отменить этот фильтр. В драйвере для этого предусматривалось несколько способов (отличающихся количеством циклов, требуемых на Intel Pentium 2 для обработки изображения), но мне не удалось корректно переписать какой-либо из этих вариантов на Go. Поэтому я пошёл другим путём: отыскал библиотеку TIFF, в которой была функция для этой цели и воспользовался ею.

Закончив с этим, я направил веб-камеру на радужный ремешок. Переключил в режим GRBG, и вот что получилось:

Это первое цветное изображение, которое мне удалось получить из этого драйвера! Просто восторг! Далее я настроил камеру и навёл её на тестовую палитру: посмотреть, как она справится. Получилось забавно:

Что лишний раз напоминает, какой долгий путь прошла эволюция веб-камер с 1999 года.

К чести QuickCam добавлю, что удручающий цветовой отклик отчасти объясним: просто я пользовался теми параметрами, которые взял из единичного USB-пакета. Поскольку такие управляющие пакеты контролируют, в частности, выдержку и яркость, эти настройки в своём роде «застыли» в тот момент, когда я впервые протестировал веб-камеру.

Интересно, что баланс белого требуется обрабатывать на стороне драйвера. Также выяснилось, что функция яркости веб-камеры целиком контролируется на стороне драйвера, а не в самом устройстве. Полагаю, в 1999 году это было целесообразно, но подозреваю (не наверняка), что сегодня многие из этих функций контролируются именно на стороне веб-камер. Эх, да сегодня даже бывают веб-камеры, на которых работает Linux!

Передача информации обратно в ядро

Всё это не очень полезно, если я не смогу подключиться с такой камеры к Google meet, поэтому давайте попробуем воспользоваться петлевым интерфейсом V4L2, чтобы сымитировать видеоустройство и попытаться залить новоиспечённые снимки с веб-камеры обратно в ядро. Так ими смогут пользоваться приложения, например Google meet!

Сделать это на самом деле легко, так как всю сложную работу можно поручить ffmpeg. Поэтому нам всего лишь требуется передать FFMPEG поток MJPEG и сообщить устройство.

Сначала создаём устройство V4L2:

sudo modprobe v4l2loopback exclusive_caps=1

А потом можем залить MJPEG как в веб-камеру при помощи примерно следующего кода:

ffmpeg -f mjpeg -i - -pix_fmt yuv420p -f v4l2 /dev/video0

Для максимальной простоты я организовал, чтобы мой драйвер пользовательского пространства делал это автоматически, и, не успел я опомниться, как он смог загрузить google meet и показать через веб-камеру, как бы смотрелась моя физиономия в 1999 году (с причудливыми артефактами постобработки, понятия не имею, откуда они взялись).

Миссия выполнена. Мы превратили винтажный электронный мусор в страшненькую, но вполне рабочую веб-камеру. Самое приятное, что попутно я узнал и много нового об ужасах USB!

Если вдруг и у вас найдётся такая камера, то весь код к описанному здесь исследованию вы можете взять здесь.

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