Почти четыре года назад я писал о реверс-инжиниринге Stream Deck с целью получения полного контроля над устройством и устранения зависимости от ПО Stream Deck. Мне по-прежнему нравится это «железо», но ПО стало только хуже — теперь оно даже требует входа в аккаунт пользователя для скачивания расширений.
Я стремлюсь максимально уважать приватность и выбор пользователей, поэтому если уж я хочу использовать устройство без аккаунта, то вам лучше предоставить мне такую возможность. К счастью, развивая идеи моей предыдущей работы над DeckSurf, я наконец-то набрался решимости вложиться по полной и сделать мой проект приемлемой альтернативой проприетарному ПО для этого крайне гибкого и универсального устройства с кнопками.
В этом посте мы рассмотрим работу Stream Deck Plus — устройства ценой $179,99, которым вы, дорогой читатель, теперь сможете пользоваться, даже если не хотите устанавливать ПО его производителя.
С чем мы будем работать
Вот рассматриваемое нами устройство:
Stream Deck Plus
Stream Deck Plus имеет особенности, которые я должен вкратце описать, прежде чем приступать к подробному процессу реверс-инжиниринга:
- 8 кнопок. По сути, этим он ничем не отличается от других устройств Stream Deck. Количество может меняться, но их поведение одинаково.
- Узкий экран. Под кнопками есть узкий цветной экран, на котором может отображаться вспомогательная контекстная информация.
- 4 поворотных ручки. Каждую ручку можно бесконечно крутить влево или вправо. Также их можно нажимать (кликать).
Мы подробно разберём каждую из особенностей и способ их работы. Кроме того, скажу, что для выполнения реверс-инжиниринга этого устройства я использовал следующий инструментарий:
- Wireshark c USBPcap.
- Виртуальная машина, к которой из USB-устройств подключён только Stream Deck Plus.
- ПО Stream Deck для изучения передаваемого по проводам трафика.
Stream Deck — это стандартное HID-устройство, не требующее для своей работы установки ПО Stream Deck. То есть после реверс-инжиниринга протокола оборудования я смогу создать собственное клиентское ПО, которое позволит выполнять любые нужные мне действия и никак не зависеть от стека ПО Elgato.
Подготовка к процессу исследования
Для начала запустим Wireshark и выберем интерфейс перехвата USB.
При некоторых конфигурациях машины может быть доступно несколько интерфейсов. Возможно, вам придётся перебрать несколько, прежде чем вы найдёте подключённый Stream Deck.
Чтобы найти устройство Stream Deck Plus в моём случае, я могу выполнить фильтрацию по product ID (PID), то есть 0x0084
(на всякий случай: Elgato VID — это 0x0FD9
). Ограничение трафика только до подключённого устройства Stream Deck Plus можно выполнить, применив следующую строку фильтра:
usb.idProduct == 0x0084
После этого Stream Deck появится в списке (вероятно, в огромном количестве другого USB-трафика):
Исходя из показанного на изображении, мне нужно найти 3.5.0
. Это значение также является destination — «адресом» устройства, который можно использовать для изучения исходящего трафика, например, задающего яркость или изображения на разных поверхностях Stream Deck.
Имея эти данные, я могу задать следующую строку фильтра:
usb.dst matches "3\\.5\\..*"
Постойте-ка… Выше мы сказали, что нам нужен адрес 3.5.0
, но во второй строке фильтра мы ищем всё, что соответствует паттерну 3.5.
. Почему?
А вы внимательны! И в самом деле, я выполняю фильтрацию не только по 3.5.0
. USB-адрес состоит из трёх компонентов — шины, устройства и конечной точки. В нашем случае Stream Deck Plus работает как шина 3, устройство 5 и конечная точка 0, но также мы должны знать и то, что одно устройство может иметь множественные конечные точки. Поэтому чтобы правильно изучать весь трафик устройства Stream Deck Plus, мы исключим идентификатор конечной точки.
Для этого можно просто воспользоваться следующей строкой фильтра:
usb.dst ~ "3.5"
Так вывод будет чуть чище и нам не придётся беспокоиться о регулярных выражениях относительного ограничения.
Разобравшись с основами, давайте начнём разбирать, как оборудование взаимодействует с моим компьютером, и наоборот.
Оборудование Stream Deck
▍ Кнопки
Кнопки ведут себя так же, как у разобранного нами ранее Stream Deck XL. Я очень благодарен разработчикам за согласованность; думаю, с точки зрения удобства поддержки это логично — если вы создали более-менее работающий API, то зачем менять его действия при смене версий оборудования? Спасибо Elgato за это.
Каждая кнопка поддерживает цветное изображение размером 120×120. Его содержимое динамически обновляется не устройством, а хостом, то есть компьютером, к которому оно подключено. Если на кнопке отображается обновлённый статус, то это потому, что компьютер постоянно передаёт на Stream Deck изображения. В Windows и macOS этой работой обычно занимается ПО Stream Deck (которое я намерен полностью заменить на DeckSurf).
▍ Работа с изображениями
Когда в компьютере задаётся изображение, то по проводу передаётся обычно сжатое (если вы используете более высокое разрешение) JPEG-изображение, а также некоторые другие общие метаданные пакетов.
Задающие изображение пакеты можно выявить, поискав в заголовке следующий паттерн (значения указаны в шестнадцатеричном виде):
+----------+----+----+----+----+----+----+----+----+
| Байт | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+----------+----+----+----+----+----+----+----+----+
| Значение | 02 | 07 | 18 | 00 | F8 | 03 | 00 | 00 |
+----------+----+----+----+----+----+----+----+----+
Заголовок можно описать так:
Индекс байта | Описание |
---|---|
0 |
Всегда 02 |
1 |
Всегда 07 |
2 |
Шестнадцатеричный ID кнопки, которой задаётся изображение. Это значение индексируется от нуля. |
3 |
Определяет, является ли текущий пакет последним пакетом, задающим изображение. Крупные изображения разбиваются на несколько пакетов, и это значение может быть равным или 00 , или 01 . |
4 и 5 |
16-битное описание в Little Endian длины полезной нагрузки изображения в текущем пакете. |
6 и 7 |
16-битное описание в Little Endian итерации (или страницы) с отсчётом от нуля для случаев, когда изображение разбито на насколько пакетов. |
Всё, что идёт после заголовка — это полезная нагрузка изображения. Если изображение разбито на части, то мы увидим несколько пакетов:
Эти пакеты URB_INTERRUPT out
нам и нужны. Не волнуйтесь, если пока не разобрались в этой терминологии. URB
расшифровывается как «USB Request Block», эти структуры используются для описания передачи USB между хостом и устройством. INTERRUPT
обозначает тип передачи. Передача USB interrupt предназначена для устройств, требующих обмена данных с низкими задержками, обычно для небольших объёмов данных. Такими устройствами могут быть клавиатуры, мыши или, как в нашем случае, Stream Deck Plus. По сути, это устройства, передающие или получающие частые обновления. Наконец, out
обозначает направление передачи — от (out) хоста к устройству. URB_INTERRUPT out
значит, что хост (мой компьютер) отправляет данные на устройство в передаче прерыванием.
Теперь обратим внимание на то, что передаваемые пакеты однородны — все они имеют длину 1051 байт. Первый пакет содержит заголовок JPEG (начальные байты изображения), а во всех последующих содержится остальное изображение, разбитое на блоки. Заголовок всегда имеет размер 8 байтов, а содержимое (полезная нагрузка изображения) объявляется в заголовке, но обычно равна 1016 байтам, благодаря чему общая полезная нагрузка составляет 1024 байта.
Однако нужно всегда проверять заголовок на реальную длину, потому что не стоит полагаться на подобные допущения, как на истину, ведь в будущем может всё поменяться.
Чтобы убедиться, что изображение задано, можно воспользоваться магией командной строки. В Wireshark выберем содержащие изображение пакеты URB_INTERRUPT out
. Чтобы их было проще находить, можно применить более строгий фильтр:
usb.dst ~ "3.5" && _ws.col.info == "URB_INTERRUPT out"
Этот фильтр будет искать пакеты URB_INTERRUPT out
только для конкретного destination. Как я уже говорил выше, выберем пакеты, а затем в меню File выберем Export Specified Packets….
В разделе Packet Range следующего диалогового окна нажмём на Selected packets only.
Дадим файлу осмысленное имя и сохраним куда-нибудь на диск. Затем мы используем инструмент командной строки из комплекта Wireshark под названием tshark
.
В Windows tshark
обычно находится в папке установки Wireshark. В моём случае он находился в C:\Program Files\Wireshark
:
Для удобства пользования tshark
можно добавить путь до Wireshark к системной переменной среды PATH
. Если сделать это, то мы сможем вызвать выполнить в терминале следующую команду:
tshark -r .\test-image-extraction.pcapng -T fields -e usb.capdata > data.txt
Она извлекает данные HID и сохраняет их дамп в текстовый файл. Так как мы уже работаем с файлом *.pcapng
, который содержит только интересующие нас пакеты изображения, нам не нужно больше заниматься фильтрацией и достаточно сохранить всё в текстовый файл.
Его содержимое будет выглядеть так:
Не особо наглядно, но мы можем заметить то, о чём я говорил выше, например, начало заголовка 02 07
. В качестве быстрого «хака» для выполнения дампа данных изображения из этого текстового файла я написал скрипт PowerShell:
param (
[string]$DataFile,
[string]$OutputFileName
)
function Process-HIDData {
param (
[string]$DataFile,
[string]$OutputFileName
)
$lines = Get-Content -Path $DataFile
$imageBytes = @()
foreach ($line in $lines) {
$hexBytes = $line.Trim()
if ($hexBytes.Length -gt 16) {
$processedBytes = $hexBytes.Substring(16)
$byteArray = for ($i = 0; $i -lt $processedBytes.Length; $i += 2) {
[Convert]::ToByte($processedBytes.Substring($i, 2), 16)
}
$imageBytes += $byteArray
}
}
$binaryData = [byte[]]::new($imageBytes.Length)
[System.Array]::Copy($imageBytes, $binaryData, $imageBytes.Length)
$scriptDirectory = $PSScriptRoot
$outputFilePath = Join-Path -Path $scriptDirectory -ChildPath $OutputFileName
[System.IO.File]::WriteAllBytes($outputFilePath, $binaryData)
Write-Output "Image saved as $outputFilePath"
}
if (-not $DataFile) {
Write-Error "Data file path is required."
exit 1
}
if (-not $OutputFileName) {
Write-Error "Output file name is required."
exit 1
}
Process-HIDData -DataFile $DataFile -OutputFileName $OutputFileName
На самом деле, он просто вырезает первые 8 байтов из каждой строки (каждая строка — это блок данных HID) и сохраняет двоичное представление как файл JPEG. Запустить его можно так:
.\exportimage.ps1 -DataFile .\data.txt -OutputFile image.jpg
После выполнения скрипта мы увидим моё тестовое изображение с ёжиком:
Отлично! Теперь мы имеем представление о том, как изображения передаются по проводу. Но ещё одна особенность кнопок Stream Deck заключается в том, что, как и любые другие кнопки, их можно нажимать. Я уже говорил об этом в предыдущем посте; если вкратце, то мы должны смотреть на обратное тому, что мы делали с изображениями.
То есть, строка фильтра теперь должна выглядеть так (не забудьте изменить аргумент src
):
_ws.col.info == "URB_INTERRUPT in" && usb.src == 3.5.1
Мне нужны данные прерывания, поступающие in (в хост) от USB-устройства 3.5.1
. Как оказывается, эти данные легко парсить, потому что с каждым нажатием и отпусканием кнопки мы получаем в данных HID всю схему кнопок.
Первые четыре байта — это заголовок, который мы можем игнорировать. Третий байт всегда обозначает количество кнопок на панели, то есть 8 в случае Stream Deck Plus. Для Stream Deck XL это значение равно 32. Также третий байт означает, какое количество байтов после заголовка содержит схему кнопок. То есть если мы нажмём на четвёртую кнопку Stream Deck Plus, то на компьютер будут переданы следующие данные:
0000 01 00 08 00 00 00 00 01 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Это ещё одна часть инфраструктуры оборудования, которая не меняется для разных моделей устройств, за что я очень благодарен Elgato.
▍ Экран
Теперь давайте поговорим о втором компоненте Stream Deck Plus: об экране. Это узкая полоса, на которой можно отображать различную информацию. По умолчанию она используется для отображения информации находящихся под ней ручек. Из этого мы можем предположить, что на этом экране есть четыре отдельные части, связанные с каждой ручкой, но это предположение будет верным только отчасти.
На практике, вся площадь экрана — это просто одно большое изображение. Условно большое — его размер 800×100. Я обнаружил это, выбрав для экрана фон, а затем выполнив описанный выше «хак» с PowerShell (но со смещением заголовка в 16 байтов вместо 8), чтобы исследовать трафик с моего компьютера к Stream Deck Plus. Я увидел следующее:
ПО Stream Deck создаёт составное изображение всего того, что пользователь связал с ручками, а затем передаёт всё это на устройство за раз в одном блобе. Если изучить исходящий трафик от хоста к устройству, мы увидим подобные пакеты:
0000 02 0C 00 00 00 00 20 03 64 00 00 00 00 F0 03 00 ...... .d....ð..
0010 FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 01 ÿØÿà..JFIF......
0020 00 01 00 00 FF DB 00 43 00 03 02 02 03 02 02 03 ....ÿÛ.C........
0030 03 03 03 04 03 03 04 05 08 05 05 04 04 05 0A 07 ................
0040 07 06 08 0C 0A 0C 0C 0B 0A 0B 0B 0D 0E 12 10 0D ................
0050 0E 11 0E 0B 0B 10 16 10 11 13 14 15 15 15 0C 0F ................
0060 17 18 16 14 18 12 14 15 14 FF DB 00 43 01 03 04 .........ÿÛ.C...
0070 04 05 04 05 09 05 05 09 14 0D 0B 0D 14 14 14 14 ................
Теперь у нас есть 16-байтный заголовок, имеющий следующую структуру (судя по начальному пакету, который использовался для отправки изображения):
+----------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| Байт | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
+----------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| Значение | 02 | 0C | 00 | 00 | 00 | 00 | 20 | 03 | 64 | 00 | 00 | 00 | 00 | F0 | 03 | 00 |
+----------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
Изучив все пакеты одного изображения, я начал записывать следующие предположения о заголовках установки экранного изображения:
Индекс байта | Описание |
---|---|
0 |
Всегда 02 |
1 |
Всегда 0C |
С 2 по 5 |
Всегда 00 |
6 |
Всегда 20 |
7 |
Всегда 03 |
8 |
Всегда 64 |
9 |
Всегда 00 |
10 |
Определяет, является ли этот блок (то есть «страница») последним при установке изображения в пакете из нескольких частей. Может иметь значение 00 или 01 . |
11 |
Индекс блока («страницы») при установке изображения пакетом из нескольких частей. |
12 |
Всегда 00 |
13 и 14 |
Обозначение в Little Endian длины полезной нагрузки |
15 |
Всегда 00 |
И всё это бы казалось вполне приемлемым, если бы не неожиданный поворот. Как я говорил выше, изначальное предположение о том, что весь экран целиком всегда управляется как единое изображение, верно только отчасти, потому что при использовании одной из ручек (нажатии или повороте) на экране на короткое время появляется оверлей, показывающий, что происходит.
Такими оверлеями, как и всем остальным, управляет ПО Stream Deck. Однако любопытно в них то, что они отправляются как сегмент. ПО отправляет не полное составное изображение, а только одну часть экрана.
Может ли быть так, что каждый сегмент экрана адресуем? Я начал сравнивать заголовки наборов оверлеев. В данных ниже показаны заголовки как отдельные байты, за которыми следует краткий фрагмент полезной нагрузки изображения (поверьте, вы не захотели бы изучать всё это целиком). Чтобы упростить анализ, я разделил экран на четыре сегмента слева направо — A, B, C и D.
▍ Сегмент A
02 0c 00 00 00 00 c8 00 64 00 00 00 00 f0 03 00 ffd8ffe000104a46
02 0c 00 00 00 00 c8 00 64 00 00 01 00 f0 03 00 62719049eff74526
02 0c 00 00 00 00 c8 00 64 00 00 02 00 f0 03 00 6b8844808f976955
02 0c 00 00 00 00 c8 00 64 00 00 03 00 f0 03 00 e7d81a2c2bbb151b
02 0c 00 00 00 00 c8 00 64 00 00 04 00 f0 03 00 0f7c678f7a666ddf
02 0c 00 00 00 00 c8 00 64 00 00 05 00 f0 03 00 434b7255d6e66e9d
02 0c 00 00 00 00 c8 00 64 00 00 06 00 f0 03 00 fc54da1adeecad78
02 0c 00 00 00 00 c8 00 64 00 00 07 00 f0 03 00 7e4095f72f5c6bc2
02 0c 00 00 00 00 c8 00 64 00 00 08 00 f0 03 00 918ba9de8b9d4edf
02 0c 00 00 00 00 c8 00 64 00 00 09 00 f0 03 00 12ebd3dd2116a218
02 0c 00 00 00 00 c8 00 64 00 01 0a 00 0a 00 00 d38bb9152295be67
▍ Сегмент B
02 0c c8 00 00 00 c8 00 64 00 00 00 00 f0 03 00 ffd8ffe000104a46
02 0c c8 00 00 00 c8 00 64 00 00 01 00 f0 03 00 7662eade285d4963
02 0c c8 00 00 00 c8 00 64 00 00 02 00 f0 03 00 23241008207b8c96
02 0c c8 00 00 00 c8 00 64 00 00 03 00 f0 03 00 920734728fdaf35d
02 0c c8 00 00 00 c8 00 64 00 00 04 00 f0 03 00 f41d3afd723f98ed
02 0c c8 00 00 00 c8 00 64 00 00 05 00 f0 03 00 4b14e7701b82b291
02 0c c8 00 00 00 c8 00 64 00 00 06 00 f0 03 00 78f61540d6b165b6
02 0c c8 00 00 00 c8 00 64 00 00 07 00 f0 03 00 481bb70eb8fc7fcf
02 0c c8 00 00 00 c8 00 64 00 01 08 00 05 02 00 fce4dbc726e6ea09
▍ Сегмент C
02 0c 90 01 00 00 c8 00 64 00 00 00 00 f0 03 00 ffd8ffe000104a46
02 0c 90 01 00 00 c8 00 64 00 00 01 00 f0 03 00 1260fa52b10e4d00
02 0c 90 01 00 00 c8 00 64 00 00 02 00 f0 03 00 e52b2649911c1ce1
02 0c 90 01 00 00 c8 00 64 00 00 03 00 f0 03 00 8cadbf5d1a6bf53d
02 0c 90 01 00 00 c8 00 64 00 00 04 00 f0 03 00 51e403cc601c8127
02 0c 90 01 00 00 c8 00 64 00 00 05 00 f0 03 00 bfd26ffc7fa25e69
02 0c 90 01 00 00 c8 00 64 00 00 06 00 f0 03 00 91d352f3fe933f3d
02 0c 90 01 00 00 c8 00 64 00 00 07 00 f0 03 00 e90ffb69fc22540d
02 0c 90 01 00 00 c8 00 64 00 01 08 00 13 01 00 9f434156b8d8df76
▍ Сегмент D
02 0c 58 02 00 00 c8 00 64 00 00 00 00 f0 03 00 ffd8ffe000104a46
02 0c 58 02 00 00 c8 00 64 00 00 01 00 f0 03 00 9e49ea2ad547ccd3
02 0c 58 02 00 00 c8 00 64 00 00 02 00 f0 03 00 33d453a105caa44e
02 0c 58 02 00 00 c8 00 64 00 00 03 00 f0 03 00 2d7d0e89d08caad3
02 0c 58 02 00 00 c8 00 64 00 00 04 00 f0 03 00 d7b45dcc67503a1e
02 0c 58 02 00 00 c8 00 64 00 00 05 00 f0 03 00 9e0ed72437f7b6f6
02 0c 58 02 00 00 c8 00 64 00 00 06 00 f0 03 00 b8b9d1aea3d7a351
02 0c 58 02 00 00 c8 00 64 00 01 07 00 a4 00 00 4e735a459338a458
▍ Ищем дельту
Среди перечисленных выше пакетов в заголовке менялись только третий и четвёртый байты. Я решил, что это адреса экранных сегментов. У нас есть:
Сегмент | Адрес |
---|---|
A |
00 00 |
B |
C8 00 |
C |
90 01 |
D |
58 02 |
Если преобразовать значения в десятичный вид, то таблица внезапно станет гораздо понятнее:
Сегмент4 | Адрес4 | Адрес Little-Endian4 |
---|---|---|
A |
00 00 |
0 |
B |
C8 00 |
200 |
C |
90 01 |
400 |
D |
58 02 |
600 |
Потрясающе. Третий и четвёртый байт в заголовке обозначает смещение в пикселях (как я говорил выше, полное изображение имеет размер 800×100). Теперь давайте сравним все эти пакеты с тем, что мы видим, когда задаём полное изображение:
02 0c 00 00 00 00 20 03 64 00 00 00 00 f0 03 00 ffd8ffe000104a46
02 0c 00 00 00 00 20 03 64 00 00 01 00 f0 03 00 89b062cee0cd8e84
02 0c 00 00 00 00 20 03 64 00 00 02 00 f0 03 00 5805c9c63771d327
02 0c 00 00 00 00 20 03 64 00 00 03 00 f0 03 00 3dab5a71737721b3
02 0c 00 00 00 00 20 03 64 00 00 04 00 f0 03 00 618246734b6342fe
02 0c 00 00 00 00 20 03 64 00 00 05 00 f0 03 00 aa159464a0cce71d
02 0c 00 00 00 00 20 03 64 00 00 06 00 f0 03 00 8996e51482344335
02 0c 00 00 00 00 20 03 64 00 00 07 00 f0 03 00 45ae683e24bbff00
02 0c 00 00 00 00 20 03 64 00 00 08 00 f0 03 00 8bc53378a7c07e1c
02 0c 00 00 00 00 20 03 64 00 00 09 00 f0 03 00 ff005ff807e3af0b
02 0c 00 00 00 00 20 03 64 00 00 0a 00 f0 03 00 b299080709c9c02a
02 0c 00 00 00 00 20 03 64 00 00 0b 00 f0 03 00 4fb45746ef817c56
02 0c 00 00 00 00 20 03 64 00 00 0c 00 f0 03 00 c62390ac84897272
02 0c 00 00 00 00 20 03 64 00 00 0d 00 f0 03 00 27030d8e36a8e001
02 0c 00 00 00 00 20 03 64 00 00 0e 00 f0 03 00 d95ab7fa562dd956
02 0c 00 00 00 00 20 03 64 00 00 0f 00 f0 03 00 ba2eaf650de69fa4
02 0c 00 00 00 00 20 03 64 00 00 10 00 f0 03 00 efe19c9f0f3c1da1
02 0c 00 00 00 00 20 03 64 00 00 11 00 f0 03 00 7e140462b850800c
02 0c 00 00 00 00 20 03 64 00 00 12 00 f0 03 00 ec37b1e8da7b416d
02 0c 00 00 00 00 20 03 64 00 00 13 00 f0 03 00 ee1636963dec088e
02 0c 00 00 00 00 20 03 64 00 00 14 00 f0 03 00 3f06e9f2c02596f4
02 0c 00 00 00 00 20 03 64 00 00 15 00 f0 03 00 49a0d8d94b7b55d3
02 0c 00 00 00 00 20 03 64 00 00 16 00 f0 03 00 8a2ea63df238c374
02 0c 00 00 00 00 20 03 64 00 00 17 00 f0 03 00 f4b8d34433bc65c4
02 0c 00 00 00 00 20 03 64 00 00 18 00 f0 03 00 2033824b02a3712a
02 0c 00 00 00 00 20 03 64 00 00 19 00 f0 03 00 1c8ee4f526bf4da7
02 0c 00 00 00 00 20 03 64 00 00 1a 00 f0 03 00 e3da788745f19f88
02 0c 00 00 00 00 20 03 64 00 00 1b 00 f0 03 00 2755734755b7dc43
02 0c 00 00 00 00 20 03 64 00 00 1c 00 f0 03 00 e0cad297dd8f5c35
02 0c 00 00 00 00 20 03 64 00 00 1d 00 f0 03 00 c00217a1cfd8f259
02 0c 00 00 00 00 20 03 64 00 00 1e 00 f0 03 00 c1cfc8ec33f91ac7
02 0c 00 00 00 00 20 03 64 00 00 1f 00 f0 03 00 b6f30aecddb72492
02 0c 00 00 00 00 20 03 64 00 00 20 00 f0 03 00 ff00b3fb552c8b08
02 0c 00 00 00 00 20 03 64 00 00 21 00 f0 03 00 baf3fc47d99f25fe
02 0c 00 00 00 00 20 03 64 00 00 22 00 f0 03 00 c6adc4510f3644f3
02 0c 00 00 00 00 20 03 64 00 00 23 00 f0 03 00 dcb5a79842477124
02 0c 00 00 00 00 20 03 64 00 00 24 00 f0 03 00 73950000a48af069
02 0c 00 00 00 00 20 03 64 00 00 25 00 f0 03 00 032c919120c6eca0
02 0c 00 00 00 00 20 03 64 00 00 26 00 f0 03 00 63f0d3e1a787127d
02 0c 00 00 00 00 20 03 64 00 00 27 00 f0 03 00 6efb25e5add3b7a9
02 0c 00 00 00 00 20 03 64 00 00 28 00 f0 03 00 7874fe1883c53e1d
02 0c 00 00 00 00 20 03 64 00 00 29 00 f0 03 00 20d4632b5ceda74e
02 0c 00 00 00 00 20 03 64 00 01 2a 00 92 02 00 bf32e9c5367927c4
Седьмой и восьмой байты внезапно оказываются 20 03
, потому что мы снова должны смотреть значения в представлении Little Endian. Для каждого сегмента это значение было равно C8 00
, то есть 200
в десятичном виде. 20 03
— это 800
, то есть ширина изображения. 64 00
— это 100
, то есть высота изображения.
Моя таблица предположений теперь становится гораздо логичнее:
Индекс байта | Описание |
---|---|
0 |
Всегда 02 |
1 |
Всегда 0C |
2 and 3 |
Смещение от левого края |
4 и 5 |
Всегда 00 00 . |
6 и 7 |
Ширина изображения |
8 и 9 |
Высота изображения |
10 |
Определяет, является ли этот блок («страница») последним при установке изображения пакетом из нескольких частей. Может быть равен 00 или 01 . |
11 и 12 |
Индекс блока («страницы») при установке изображения при помощи пакета из нескольких частей. |
13 и 14 |
Обозначение в Little Endian длины полезной нагрузки |
15 |
Всегда 00 |
Вот и всё, теперь мы знаем, как задаются данные экрана! Всё оказалось не так сложно, когда я начал изучать разницу между пакетами.
Последнее, что нам нужно рассмотреть: экран является сенсорным, так что мы должны уметь распознавать нажатия на сегменты. Нажатие на любую часть экрана — это функциональный эквивалент нажатия на ручку — при использовании ПО Stream Deck будет отображаться тот же оверлей. Но как это отражается в Wireshark?
Чтобы проверить это, давайте снова зададим фильтр, потому что нам нужно отслеживать события, происходящие в устройстве и передаваемые на PC:
usb.src ~ "3.5" && _ws.col.info == "URB_INTERRUPT in"
Касаясь экрана слева направо, мы получаем следующие данные:
01 02 0e 00 01 01 47 00 40 00 00000000000000000
01 02 0e 00 01 01 07 01 1c 00 00000000000000000
01 02 0e 00 01 01 ee 01 32 00 00000000000000000
01 02 0e 00 01 01 b3 02 29 00 00000000000000000
Они выглядят очень произвольно, слишком разнообразно, чтобы мы могли прийти к каким-то конкретным выводам. Давайте попробуем понажимать на разные части одного и того же сегмента сенсорного экрана:
01 02 0e 00 01 01 b2 02 33 00 00000000000000000
01 02 0e 00 01 01 13 03 20 00 00000000000000000
01 02 0e 00 01 01 a9 02 46 00 00000000000000000
01 02 0e 00 01 01 06 03 26 00 00000000000000000
01 02 0e 00 01 01 bb 02 20 00 00000000000000000
01 02 0e 00 01 01 00 03 52 00 00000000000000000
01 02 0e 00 01 01 b6 02 3e 00 00000000000000000
01 02 0e 00 01 01 f7 02 2f 00 00000000000000000
01 02 0e 00 01 01 f1 02 1c 00 00000000000000000
01 02 0e 00 01 01 8d 02 1c 00 00000000000000000
Такое разнообразие значений сразу дало мне понять, что это координаты экрана! Структура оказалась такой:
+----------+----+----+----+----+----+----+----+-----+----+-----+
| Байт | 0 | 1 | 2 | 3 | 4 | 5 | 6 - 7 | 8 - 9 |
+----------+----+----+----+----+----+----+----+-----+----+-----+
| Значение | 01 | 02 | 0E | 00 | 01 | 01 | Коорд. X | Коорд. Y |
+----------+----+----+----+----+----+----+----+-----+----+-----+
В отличие от ситуации с кнопками, здесь не фиксируется событие отпускания, мы просто регистрируем касание. Это значит, что сразу после получения события из показанного выше заголовка мы можем на основании переданных координат X и Y определить, какой части экрана коснулся пользователь, и выполнять соответствующие действия.
▍ Ручки
Также нам следует поговорить о ручках. Каждую ручку можно использовать тремя способами:
- Поворачивать вправо.
- Поворачивать влево.
- Нажимать.
Чтобы в логах всё фиксировалось правильно, я пометил каждую ручку аналогично экранным сегментам и сделал четыре поворота вправо, потом четыре поворота влево и нажимал на ручку. Ниже представлены полученные данные.
▍ Ручка A
▍ Повороты вправо
01 03 05 00 01 01 00 00 00 0000000000
01 03 05 00 01 01 00 00 00 0000000000
01 03 05 00 01 01 00 00 00 0000000000
01 03 05 00 01 01 00 00 00 0000000000
▍ Повороты влево
01 03 05 00 01 ff 00 00 00 0000000000
01 03 05 00 01 ff 00 00 00 0000000000
01 03 05 00 01 ff 00 00 00 0000000000
01 03 05 00 01 ff 00 00 00 0000000000
▍ Нажатие
01 03 05 00 00 01 00 00 00 0000000000
01 03 05 00 00 00 00 00 00 0000000000
▍ Ручка B
▍ Повороты вправо
01 03 05 00 01 00 01 00 00 0000000000
01 03 05 00 01 00 01 00 00 0000000000
01 03 05 00 01 00 01 00 00 0000000000
01 03 05 00 01 00 01 00 00 0000000000
▍ Повороты влево
01 03 05 00 01 00 ff 00 00 0000000000
01 03 05 00 01 00 ff 00 00 0000000000
01 03 05 00 01 00 ff 00 00 0000000000
01 03 05 00 01 00 ff 00 00 0000000000
▍ Нажатие
01 03 05 00 00 00 01 00 00 0000000000
01 03 05 00 00 00 00 00 00 0000000000
▍ Ручка C
▍ Повороты вправо
01 03 05 00 01 00 00 01 00 0000000000
01 03 05 00 01 00 00 01 00 0000000000
01 03 05 00 01 00 00 01 00 0000000000
01 03 05 00 01 00 00 01 00 0000000000
▍ Повороты влево
01 03 05 00 01 00 00 ff 00 0000000000
01 03 05 00 01 00 00 ff 00 0000000000
01 03 05 00 01 00 00 ff 00 0000000000
01 03 05 00 01 00 00 ff 00 0000000000
▍ Нажатие
01 03 05 00 00 00 00 01 00 0000000000
01 03 05 00 00 00 00 00 00 0000000000
▍ Ручка D
▍ Повороты вправо
01 03 05 00 01 00 00 00 01 0000000000
01 03 05 00 01 00 00 00 01 0000000000
01 03 05 00 01 00 00 00 01 0000000000
01 03 05 00 01 00 00 00 01 0000000000
▍ Повороты влево
01 03 05 00 01 00 00 00 ff 0000000000
01 03 05 00 01 00 00 00 ff 0000000000
01 03 05 00 01 00 00 00 ff 0000000000
01 03 05 00 01 00 00 00 ff 0000000000
▍ Нажатие
01 03 05 00 00 00 00 00 01 0000000000
01 03 05 00 00 00 00 00 00 0000000000
▍ Анализируем данные
Изучая описанные выше сигналы, мы достаточно быстро можем выявить общие особенности, потому что одни и те же значения продолжают сдвигаться вправо.
По сути, это паттерн нажатия на кнопки, который мы видели при нажатии на кнопки!
+----------+----+----+----+----+---------+------------------+------------------+------------------+------------------+
| Байт | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+----------+----+----+----+----+---------+------------------+------------------+------------------+------------------+
| Значение | 01 | 03 | 05 | 00 | Поворот | Действие ручки A | Действие ручки B | Действие ручки C | Действие ручки D |
+----------+----+----+----+----+---------+------------------+------------------+------------------+------------------+
Для каждой ручки мы получаем следующую комбинацию:
- Байт
4
получает значение01
(поворот), относящиеся к кнопке байты получают значения01
(поворот вправо) илиFF
(поворот влево). - Байт
4
получает значение00
(нажатие), относящиеся к кнопке байты получают значения01
(нажатие) или00
(отпускание).
Очень здорово: если вы прочитали всю статью, то теперь знаете, как задаются двоичные данные для каждого элемента управления Stream Deck Plus.
Пишем обёртку
Итак, разобравшись с рутиной анализа двоичных данных, можно сделать так, чтобы работать со всем этим было удобнее. Для этого я обновил DeckSurf SDK, добавив в него поддержку Stream Deck Plus.
Последний релиз DeckSurf SDK на NuGet (0.0.4) вы уже можете использовать для управления устройством Stream Deck Plus!
Вот полнофункциональный образец кода на C#, показывающий, как обрабатывать события и устанавливать изображения на Stream Deck Plus:
using DeckSurf.SDK.Core;
using DeckSurf.SDK.Models;
using DeckSurf.SDK.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
namespace DeckSurf.SDK.StartBoard
{
class Program
{
static void Main(string[] args)
{
var exitSignal = new ManualResetEvent(false);
var devices = DeviceManager.GetDeviceList();
Console.WriteLine("The following Stream Deck devices are connected:");
foreach (var connectedDevice in devices)
{
Console.WriteLine(connectedDevice.Name);
}
var device = ((List)devices)[0];
device.StartListening();
device.OnButtonPress += Device_OnButtonPress;
byte[] testImage = File.ReadAllBytes(args[0]);
var image = ImageHelpers.ResizeImage(testImage, device.ScreenWidth, device.ScreenHeight, device.IsButtonImageFlipRequired);
device.SetScreen(image, 250, device.ScreenWidth, device.ScreenHeight);
var keyImage = ImageHelpers.ResizeImage(testImage, device.ButtonResolution, device.ButtonResolution, device.IsButtonImageFlipRequired);
device.SetKey(1, keyImage);
device.SetBrightness(29);
Console.WriteLine("Done");
exitSignal.WaitOne();
}
private static void Device_OnButtonPress(object source, ButtonPressEventArgs e)
{
Console.WriteLine($"Button with ID {e.Id} was pressed. It's identified as {e.ButtonKind}. Event is {e.EventKind}. If this is a touch screen, coordinates are {e.TapCoordinates.X} and {e.TapCoordinates.Y}. Is knob rotated: {e.IsKnobRotating}. Rotation direction: {e.KnobRotationDirection}.");
}
}
}
В этом примере предполагается, что устройство Stream Deck Plus подключено первым (индекс 0), поэтому, возможно, в код придётся внести изменения, если к компьютеру подключено несколько Stream Deck. Тем не менее, этот пример показывает, насколько просто выполнять взаимодействия с Stream Deck при помощи DeckSurf SDK. Это ещё очень ранняя версия, поэтому в процессе движения к стабильной версии в будущем что-то может ломаться, но пока можете экспериментировать!
Самая актуальная документация доступна на https://docs.deck.surf
.
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻