Недавно супруга попросила помочь с переносом видеозаписей с её GoPro на мой ноутбук. Раньше, когда камеры определялись как обычные USB-накопители с файловой системой FAT, подобных затруднений не возникало, однако с современными MTP-устройствами всё бывает совсем иначе:

Разумеется, можно было воспользоваться утилитой Android File Transfer for Linux, но я предпочел разобраться в корне проблемы самостоятельно.
Анализ дескриптора устройства
Любое исследование USB-периферии логично начинать с изучения её дескриптора. Уточним идентификаторы устройства и выгрузим его конфигурацию:
$ lsusb|grep -i gopro
Bus 001 Device 045: ID 2672:0059 GoPro HERO13 Black
$ lsusb -d2672:0059 -vvv >descriptor.txt
Содержимое полученного файла (из-за внушительного объема скрыто под спойлером):
descriptor.txt
Bus 001 Device 008: ID 2672:0059 GoPro HERO13 Black
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.10
bDeviceClass 0 [unknown]
bDeviceSubClass 0 [unknown]
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x2672 GoPro
idProduct 0x0059 HERO13 Black
bcdDevice 0.01
iManufacturer 1 GoPro
iProduct 2 HERO13 Black
iSerial 3 C3534250246817
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 0x007c
bNumInterfaces 3
bConfigurationValue 1
iConfiguration 4 Generic Config
bmAttributes 0xc0
Self Powered
MaxPower 100mA
Interface Association:
bLength 8
bDescriptorType 11
bFirstInterface 0
bInterfaceCount 2
bFunctionClass 2 Communications
bFunctionSubClass 13 [unknown]
bFunctionProtocol 0
iFunction 8 CDC NCM
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 2 Communications
bInterfaceSubClass 13 [unknown]
bInterfaceProtocol 0
iInterface 5 CDC Network Control Model (NCM)
CDC Header:
bcdCDC 1.10
CDC Union:
bMasterInterface 0
bSlaveInterface 1
CDC Ethernet:
iMacAddress 6 0457474BB944
bmEthernetStatistics 0x00000000
wMaxSegmentSize 1514
wNumberMCFilters 0x0000
bNumberPowerFilters 0
CDC NCM:
bcdNcmVersion 1.00
bmNetworkCapabilities 0x11
crc mode
packet filter
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0010 1x 16 bytes
bInterval 9
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 0
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 [unknown]
bInterfaceProtocol 1
iInterface 7 CDC Network Data
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 1
bNumEndpoints 2
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 [unknown]
bInterfaceProtocol 1
iInterface 7 CDC Network Data
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0200 1x 512 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x01 EP 1 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0200 1x 512 bytes
bInterval 0
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 2
bAlternateSetting 0
bNumEndpoints 3
bInterfaceClass 6 Imaging
bInterfaceSubClass 1 Still Image Capture
bInterfaceProtocol 1 Picture Transfer Protocol (PIMA 15470)
iInterface 10 MTP
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x83 EP 3 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0200 1x 512 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x02 EP 2 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0200 1x 512 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x84 EP 4 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x001c 1x 28 bytes
bInterval 6
Binary Object Store Descriptor:
bLength 5
bDescriptorType 15
wTotalLength 0x0016
bNumDeviceCaps 2
USB 2.0 Extension Device Capability:
bLength 7
bDescriptorType 16
bDevCapabilityType 2
bmAttributes 0x0000010e
BESL Link Power Management (LPM) Supported
BESL value 256 us
SuperSpeed USB Device Capability:
bLength 10
bDescriptorType 16
bDevCapabilityType 3
bmAttributes 0x00
wSpeedsSupported 0x000f
Device can operate at Low Speed (1Mbps)
Device can operate at Full Speed (12Mbps)
Device can operate at High Speed (480Mbps)
Device can operate at SuperSpeed (5Gbps)
bFunctionalitySupport 1
Lowest fully-functional device speed is Full Speed (12Mbps)
bU1DevExitLat 10 micro seconds
bU2DevExitLat 511 micro seconds
Device Status: 0x0001
Self Powered
Краткие выводы:
-
Интерфейсы 0 и 1 относятся к протоколу CDC Network и не представляют интереса — они обеспечивают работу камеры как сетевого адаптера.
-
Интерфейс 2 использует PTP (в частности, расширение MTP) и работает через конечные точки 0x83 (Bulk IN), 0x02 (Bulk OUT) и 0x84 (Interrupt IN).
-
Наличие точки с прерыванием (Interrupt IN) говорит о том, что камера асинхронно оповещает клиента о событиях.
Стоит заметить, что адреса этих точек не фиксированы стандартом, а динамически определяются хостом при анализе конфигурации устройства.
Диагностика через Wireshark
Следующий шаг — перехват USB-трафика. Подготовим окружение, чтобы обойтись без прав суперпользователя:
$ sudo modprobe usbmon
$ sudo chgrp wireshark /dev/usbmon*
$ sudo chmod g+r /dev/usbmon*
$ wireshark
Запустим сниффинг на интерфейсе usbmon0 с фильтром по нашему устройству и нужным точкам, чтобы отсеять лишний «шум»:
usb.device_address==30 && usb.endpoint_address in {0x83, 0x02, 0x84}
Если всё настроено верно, мы увидим обмен пакетами. Однако для глубокого анализа нам необходим специализированный диссектор протокола, и здесь на помощь придет AI.
Создание диссектора
Я воспользовался помощью Claude, скормив ему техническую документацию. В качестве основы для тестирования использовал дамп трафика (предварительно установив tshark).
Examine the docs and implement an MTP over USB protocol dissector for Wireshark 4.2.2 in Lua. You’re free to use tshark for the testing purposes. I uploaded an example of MTP communication in pcapng format for your reference — but keep in mind that it also contains CDC Network class communication that should be ignored.
Готовый скрипт mtp.lua нужно разместить в директории плагинов Wireshark (обычно /usr/lib/x86_64-linux-gnu/wireshark/plugins/), после чего перезапустить программу:

Теперь, когда коммуникация стала «читаемой», стало проще искать аномалии.
Суть проблемы
Довольно быстро удалось заметить интересную деталь: при запросе списка доступных хранилищ (Storage) от хоста, камера в ответ присылает пустой перечень.

Выяснилось, что GoPro не готова отдавать данные моментально после подключения — ей требуется пара секунд на инициализацию. По протоколу устройство должно оповестить хост событием PTP_EC_StoreAdded, однако наш Linux-стек (в лице kiod) даже не ожидал подобных уведомлений — запрос на прерывание попросту отсутствовал.
О низкоуровневых USB-транзакциях
Вкратце: клиент (например, libusb) инициирует трансфер, хост-контроллер отправляет токен IN. Если устройство не готово, оно шлет NAK, и трансфер «зависает» до готовности. ОС эти детали абстрагирует, и увидеть их можно лишь через аппаратный анализатор.
Исправление KDE
Решение — научить kiod мониторить события PTP и реагировать на них. Сначала получаем исходный код компонента:
$ apt-file search kf5/kiod/kmtpd.so
kio-extras: /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/kiod/kmtpd.so
$ apt-get source kio-extras
В недрах kio-extras/mtp/ нашлись механизмы оповещения системы о добавлении файлов. Я создал класс MTPEventWorker, который в отдельном потоке опрашивает устройство и генерирует сигналы:
void MTPEventWorker::run()
{
while (!m_stop) {
LIBMTP_event_t event;
uint32_t storage_id = 0;
const int ret = LIBMTP_Read_Event(m_device, &event, &storage_id);
if (ret != 0) break;
switch (event) {
case LIBMTP_EVENT_STORE_ADDED:
Q_EMIT storageAdded(storage_id);
break;
case LIBMTP_EVENT_STORE_REMOVED:
Q_EMIT storageRemoved(storage_id);
break;
}
}
}
Интеграция в MTPDevice выглядит следующим образом:
m_eventThread = new QThread(this);
m_eventWorker = new MTPEventWorker(m_mtpdevice);
m_eventWorker->moveToThread(m_eventThread);
connect(m_eventThread, &QThread::started, m_eventWorker, &MTPEventWorker::run);
connect(m_eventWorker, &MTPEventWorker::storageAdded, this, &MTPDevice::onStorageAdded);
m_eventThread->start();
В слоте onStorageAdded мы просто обновляем список хранилищ и отправляем сигнал KDirNotify::emitFilesAdded, чтобы файловый менеджер увидел обновленный контент. После пересборки и установки пакета проблема исчезла окончательно.
Аналогичные устройства (например, некоторые Android-смартфоны) часто решают проблему иначе — физическим переподключением через изменение состояния линии (pull-up), что KDE уже умеет корректно обрабатывать.
Итог
Надеюсь, это описание пролило свет на внутреннюю кухню USB/MTP-коммуникации и методы решения подобных «багов инициализации». Полный текст патча доступен по этой ссылке.


