Для тех, кто только подключился -- я рассказываю про платформу для VR игр, как с ней интегрироваться и как добраться до ее сенсоров напрямую.
Я уже заметил некоторые глюки и проблемы с сенсорами -- значит, неплохо было бы их поправить. Но чтобы их поправить, надо понять что они за зверь, и можно ли их как-то поменять. Из-за огромного размера статьи, пришлось разделить и без того маленький рефератик на еще более маленькие кусочки -- поэтому сегодня мы просто заглянем внутрь сенсора и научимся менять ему прошивку в простом и удобном виде.
Вскрытие покажет
Изучение чего либо обычно представляет собой хаотичный процесс с заходами с разных сторон, так называемый "метод научного тыка". Никогда заранее не скажешь, где идеальный путь, пока не посмотришь на итог процесса на ретроспективе. Поэтому не следует воспринимать данный опус как руководство к действию, скорее как небольшой обзор на что можно смотреть, да где искать крупицы полезностей. Как правило, всё множество путей помогают друг другу и в конце концов приводят к пониманию.

Но что точно хорошо почти всегда (когда есть возможность) -- это заглядывание внутрь. Если мы заглянем внутрь сенсора для ног, мы сходу заметим большой модуль, подписанный HY-40R201C. Это BLE5.0 модуль, основанный на TI CC2640R2. То есть это процессор (точнее, два) плюс радиомодуль. Может быть использован как сам по себе, загружая свою прошивку в него, так и в тандеме с внешним процессором.

Других процессоров на плате не видно, на обратной стороне ничего, кроме модуля оптической мыши A9800 не обнаружено.
Вывод: модуль используется напрямую как логический процессор. На плате присутствуют два порта для батарей, но подключена только одна. Есть кнопки Reset и Boot. Теперь мы всегда можем открыть даташит, чтобы понять где какие порты и адреса железа лежат, да знаем что это ARM Cortex M3 с ПЗУ на 128кб, рам на 8кб. Не очень много, но явно хватает.
В отличие от Nordic модуля, которым я воспользовался, поддержка USB напрямую отсутствует. Зато на плате виден CH9326, который совпадает по названию с лежащей в папке гейтвея DLLке.
Таким образом, мы либо узнали, либо подтвердили, что сенсоры общаются через USB-HID конвертер-чип, работают на ARM, и основаны на Texas Instruments SIMPLELINK-CC2640R2-SDK. К SDK еще идёт "Academy", в котором в сжатой форме можно почтитать про BLE, как его готовить внутри СДК, да немного рассмотрено примеров. В любом случае, так как девкита нет, на самом сенсоре поиграться сходу не получится, в остальном придётся пользоваться полнотекстовым поиском внутри установленного SDK.
Что в ROMушке тебе моём
Внешний осмотр это хорошо, но понять, что внутри, просто осмотром не получится. На плате есть пятачки подписанные "V R C M G", где G явно Ground, всё остальное -- вопрос. Можно прозвонить, конечно, но... JTAG у меня всё равно нет.
Тут, к счастью, можно вспомнить, что гейтвей умеет прошивать сенсоры. Я когда первый раз их подключал, он мне говорил, что прошивка старая, и надо залить новую. И залил. Вывод -- прошивка гейтвею доступна. Обзор глазами файлов в папке гейтвея:
C:\>dir /b /d "C:\Program Files (x86)\KAT Gateway\*bin" loco_ankle_by_embeded.bin loco_receiver_by_embeded.bin loco_sensor_group_application_foot_release_by_embeded_engineer.bin loco_sensor_group_application_waist_release_by_embeded_engineer.bin loco_s_ankle_by_embeded.bin loco_s_foot_by_embeded.bin loco_s_receiver_by_embeded.bin loco_s_waist_by_embeded.bin loco_waist_by_embeded.bin walk_c_foot_by_embeded.bin walk_c_hall_by_embeded.bin walk_c_receiver_by_embeded.bin walk_c_v2_foot_by_embeded.bin walk_c_v2_hall_by_embeded.bin walk_c_v2_receiver_by_embeded.bin
Прикольно, а где C2?!
C:\>dir /b /d "C:\Program Files (x86)\KAT Gateway\*hex" katvr_direction.hex katvr_foot.hex katvr_receiver.hex
Хм. Грузим dotPeek опять, Ctrl+Alt+T, ".hex"... Ха!

byte index = 0; int num1 = (int) KatvrFirmwareHelper.ch9326_find(); assert(num1 != 0) int num2 = (int) KatvrFirmwareHelper.ch9326_open(Update_Firmware_Upgrading_Form.vid, Update_Firmware_Upgrading_Form.pid); assert(num2 != 0) int num21 = KatvrFirmwareHelper.ch9326_set_gpio(index, (byte) 15, (byte) 15) assert(num21 != 0) int num3 = (int) KatvrFirmwareHelper.ch9326_connected(index); assert(num3 != 0) int num4 = (int) KatvrFirmwareHelper.flash(_hex_path, device_type, device_state); assert(num4 == 1) KatvrFirmwareHelper.ch9326_ClearThreadData(); KatvrFirmwareHelper.close_ch9326(); /* Write MACs of sensors into receiver if we updated receiver */ if (Update_Firmware_Upgrading_Form.deviceType == C2FirmwareUpdaeManager.C2DeviceType.Receiver) { KATSDKInterfaceHelper.WriteSensorPair(...) }
Прекрасно, то есть для прошивки нам потребуется пара вызовов из KatvrFirmwareHelper и гекс прошивки. Но в дизасм как правило надо грузить BIN, а не HEX. Впрочем, это решается множеством способов. Я просто запустил WSL:
$ cd /mnt/c/Program\ Files\ \(x86\)/KAT\ Gateway/ $ for k in foot direction receiver; do objcopy --input-target=ihex --output-target=binary katvr_$k.hex katvr_$k.bin; done $ ls -la kat*bin -rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_direction.bin -rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_foot.bin -rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_receiver.bin
Обращаем внимание, что все файлы ровно 128кБ, при этом hex разных размеров, значит, прошивка из нескольких секций, и, вероятно, части в ней отсутствуют или использованы для настроек, или еще что. Просто учтём на будущее.
Кстати, если мы еще не вскрывали сенсор, и не знаем что за процессор, можно попробовать потыкать в бинарничек через binwalk:
$ binwalk --disasm ./katvr_direction.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 ARM executable code, 16-bit (Thumb), little endian, at least 1624 valid instructions
или cpu_rec:
$ python cpu_rec.py ./katvr_direction.bin ./katvr_direction.bin full(0x20000) None chunk(0x10000;32) ARMhf
Где мы убеждаемся, что да, это ARM и в основном в Thumb режиме.
Потыкав дополнительно через strings:
$ strings -n 10 katvr_direction.bin inputNormal FinputGyroRv executable N]_]>CNUW]>@FUm `(i0a(}0uh}pu [USQOMKIFCA?<:8 p>"`hBp !`h k +# p(F#p !i"hQ\)pch!i pGpGpGpGpG {unknown-instance-name} {empty-instance-name} F{static-instance-name}
Заметки на полях
Поиск полезных данных никогда не бывает лишним. Например, вот этот вы��од strings, в котором есть странные строки -- "inputGyroRv" и "inputNormal". Поиск по ним на github дал сходу интересную вещь, что позволило разметить часть функций и структур, которыми пользуется сенсор направления. Для ног однако подобной фкусности не обнаружилось.
Заливка прошивки на ходу
Разглядывание документации на Serial Boot Loader показывает, что бутлоадер просто готов принимать прошивку если в него перезагрузиться, 0x55 синхронизует скорость, в общем, всё как всегда. Мы, впрочем, не можем пользоваться родным прошивальщиком, так как у нас нет последовательного интерфейса -- у нас есть HID2Serial, так что прошивка должна идти через него. Но, как мы уже выше выяснили, функции прошивки экспортированы наружу, так что можно просто попробовать вызвать их.
Создадим простой C# проект, куда импортируем KatvrFirmwareHelper и katvr_firmware.dll на которую он ссылается:
static void Main(string[] args) { uint vid = 0xC4F4u; byte device_state = 0; byte index = 0; uint pid = 28471u; byte device_type = 3; string hex_path = "C:\\Program Files (x86)\\KAT Gateway\\katvr_foot.hex"; if (KatvrFirmwareHelper.ch9326_find() == 0) { Console.WriteLine("ch9326_find failed"); return; } if (KatvrFirmwareHelper.ch9326_open(vid, pid) == 0) { Console.WriteLine("ch9326_open failed"); return; } if (KatvrFirmwareHelper.ch9326_set_gpio(index, (byte)15, (byte)15) == 0) { Console.WriteLine("ch9326_set_gpio failed"); return; } if (KatvrFirmwareHelper.ch9326_connected(index) == 0) { Console.WriteLine("ch9326_connected failed"); return; } if (KatvrFirmwareHelper.flash(hex_path, device_type, device_state) != 1) { Console.WriteLine("KatvrFirmwareHelper.flash failed"); Console.ReadKey(); return; } KatvrFirmwareHelper.ch9326_ClearThreadData(); KatvrFirmwareHelper.close_ch9326(); }
Запустим -- в консоли какие-то отладочные распечатки, ошибка. Эм... А, ну да, зажимаем Flash кнопку, тыкаем Reset, запускаем опять -- побежали точки по экрану. Через минуты полторы -- готово. Сенсор всё еще жив. Правда, мигает левой лампочкой вместо правой, как мигал до этого. Упс. Настройки стерлись.
Патчим прошивку
Но хотелось бы комфортного патчинга. У гидры есть возможность экспортировать текущий файл как Hex или Raw, но экспортируется всё, включая рам и области, которых не было в исходном hex'е. Вывод -- надо патчить прямо HEX файл, а как?
Для начала -- на чем-то надо тренироваться. Проще всего сделать простой бинарный патч -- поменяем "KATVR" строку на "KAT-F", то есть устройство будет себя анонсировать как "KAT-F" (типа Foot/нога). Открываем любым hex редактором, например, WinHex, и правим. Затем берём дифф:
> fc.exe /b .\katvr_foot_orig.bin .\katvr_foot.bin Comparing files .\katvr_foot_orig.bin and .\KATVR_FOOT.BIN 000129E9: 56 2D 000129EA: 52 46
Отлично, у нас есть патч, который легко прочитать как есть или сконвертировать с C#:
static readonly PatchEntry[] PatchFoot = { ( 0x000129e9, 0x56, 0x2D ), // V => - ( 0x000129ea, 0x52, 0x46 ), // R => F };
Из вкусных трюков, которые удалось найти для C# (я очень редко его трогаю) -- приведение тупла к структуре, позволяющая сократить многабукав в статических массивах. Просто структуру автоматически он не разбирает, но мы можем добавить конструктор и implicit operator который вызовет приведение:
struct PatchEntry { readonly public int addr; readonly public byte orig; readonly public byte patch; public PatchEntry(int addr, byte orig, byte patch) { this.addr = addr; this.orig = orig; this.patch = patch; } public static implicit operator PatchEntry((int addr, uint orig, uint patch) tuple) { return new PatchEntry(tuple.addr, (byte)tuple.orig, (byte)tuple.patch); } };
Теперь, когда у нас есть патч в удобном машино-читаемом виде, его надо наложить на HEX.
Возьмём HexIO библиотеку и накидаем фиксилку. Знать надо не много: следим за текущим адресом, если прочитанная строка включает в себя адрес патча -- исправляем. К сожалению, HexIO не следит за изменениями в структуре, и не обновляет контрольную сумму -- пришлось выкрутиться через пересоздание записи. Не очень красиво, но быстро и работает:
static string PatchHex(string input, PatchEntry[] patch) { string output = System.IO.Path.GetTempFileName() + ".hex"; IIntelHexStreamReader hexInput = new IntelHexStreamReader(input); using (StreamWriter hexOutput = new StreamWriter(output)) { uint offset = 0; var patch_i = 0; do { IntelHexRecord rec = hexInput.ReadHexRecord(); if (rec.RecordType == IntelHexRecordType.Data) { while (patch_i < patch.Length) { var pe = patch[patch_i]; if (pe.addr >= offset + rec.Offset) { long idx = pe.addr - offset - rec.Offset; if (idx >= rec.RecordLength) { break; } if (rec.Data[(int)idx] != pe.orig) { Console.WriteLine("File data doesn't match expected."); throw new InvalidDataException(); } rec.Data[(int)idx] = pe.patch; rec = new IntelHexRecord(rec.Offset, rec.RecordType, rec.Data); patch_i++; } else { Console.WriteLine("Can't apply patch to a gap."); throw new InvalidDataException(); } }; } else if (rec.RecordType == IntelHexRecordType.ExtendedLinearAddress && rec.RecordLength == 2) { offset = (uint)((rec.Data[0] << 8 | rec.Data[1]) << 16); } else if (rec.RecordType == IntelHexRecordType.EndOfFile) { if (patch_i < patch.Length) { Console.WriteLine("Not all patch was applied!"); throw new InvalidDataException(); } } else { Console.WriteLine(rec.ToString()); throw new InvalidDataException(); } hexOutput.WriteLine(rec.ToHexRecordString()); } while (!hexInput.State.Eof); }; return output; }
Теперь применим патч и используем патченный hex вместо оригинала:
static void Main(string[] args) { string orig_hex = "C:\\Program Files (x86)\\KAT Gateway\\katvr_foot.hex"; string hex_path = PatchHex(orig_hex, PatchFoot); ... }
Прошиваем, смотрим, что видно в Bluetooth окружении: ха! есть "KAT-F" устройство.
Заметки на полях:
Когда позднее потребовалось добавить немного кода в прошивку, пришлось повозиться со вставкой новых строк. Для этого вместо ошибки в Else вместо ошибки про Gap добавил кусок, напрямую конструирующий новые записи и выводящий их сразу:
} else { int start = pe.addr; List<byte> data = new List<byte>(); do { data.Add(patch[patch_i++].patch); } while (patch_i < patch.Length && patch[patch_i].addr - 1 == patch[patch_i-1].addr && patch[patch_i].addr - start < 0x20); if (start - offset >= 0x10000) { Console.WriteLine("Can't inject a record: cross boundary"); throw new InvalidDataException(); } var newrec = new IntelHexRecord((ushort)(start - offset), rec.RecordType, data); hexOutput.WriteLine(newrec.ToHexRecordString()); }
User-friendly патчинг
Так как я хочу сделать патчи, которыми могут пользоваться люди, нужно оформить прошивку в виде скрипта.
В принципе, как уже было сказано, C# переносится в PowerShell тривиально:
param ( [string]$firmware = "", [int]$dvid = 0xC4F4, [int]$dpid = 28471, [int]$type = 3, [int]$index = 0 ) Add-Type -Path "C:\Program Files (x86)\KAT Gateway\KAT_WalkC2_Dx.dll" if ($firmware -eq "") { $firmware = $katPath + "C:\Program Files (x86)\KAT Gateway\katvr_foot.hex" } Write-Host "Want to flash $firmware" if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_find() -eq 0) { throw "ch9326_find failed" } if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_open($dvid, $dpid) -eq 0) { throw "ch9326_open failed" } if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_set_gpio($index, 15, 15) -eq 0) { throw "ch9326_set_gpio failed" } if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_connected($index) -eq 0) { throw "ch9326_connected failed" } if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::flash($firmware, $type, 0) -ne 1) { throw "KatvrFirmwareHelper.flash failed" } [KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_ClearThreadData() [KAT_WalkC2_Dx.KatvrFirmwareHelper]::close_ch9326()
Уже неплохо, позволяет прошить любой патч или откатить прошивку. Но вот после прошивки сенсор теряет его настройку (левый-правый), значит, надо сделать восстановление режима спаривания.
Позаимствовав код определения спаривания из прошлых скриптов, по сути надо только добавить поиск сенсора и отправку команды
Причем и ReadDeviceId и WriteDeviceId уже есть готовые:
... $id = -1 [IBizLibrary.KATSDKInterfaceHelper]::ReadDeviceId($dev.serialNumber, [ref]$id) $sensor = New-Object IBizLibrary.KATSDKInterfaceHelper+sensorInformation [IBizLibrary.KATSDKInterfaceHelper]::GetSensorInformation([ref]$sensor, $dev.serialNumber) $leftmac = [IBizLibrary.KATSDKInterfaceHelper]::receiverPairingInfoSave.ReceiverPairingByte[7..12] $rightmac = [IBizLibrary.KATSDKInterfaceHelper]::receiverPairingInfoSave.ReceiverPairingByte[13..19] if(-not(Compare-Object $leftmac $sensor.mac)) { [IBizLibrary.KATSDKInterfaceHelper]::WriteDeviceId($dev.serialNumber, 2) Write-Host "Made the sensor to be Left Foot" } elseif(-not(Compare-Object $rightmac $sensor.mac)) { [IBizLibrary.KATSDKInterfaceHelper]::WriteDeviceId($dev.serialNumber, 3) Write-Host "Made the sensor to be Right Foot" } else { throw "The sensor's mac is not paired to the treadmill" }
Если добавить еще cmd батнички:
:: restore-foot.cmd powershell.exe -ExecutionPolicy Bypass -File flush-feet-sensor.ps1 :: update-foot.cmd powershell.exe -ExecutionPolicy Bypass -File flush-feet-sensor.ps1 --firmware .\my-foot.hex
То пользователю надо будет только запустить скрипт.
Патчим патчи, чтобы патчить патчи
Строго говоря мы не владеем исходной прошивкой, а потому распространять её как-то не оч. С другой стороны, у всех пользователей уже стоит гейтвей, то есть мы можем просто использовать исходную прошивку -- надо только наложить таки патч поверх неё. Хотелось бы избежать бинарников или слишком сложного скрипта (можно было бы перенести всю логику HEX патча с C# на PowerShell). Вывод -- надо сделать простой патчинг.
Иначе говоря, чтобы пропатчить патч, надо наложить патч. Ну вы поняли, да?

У нас теперь есть два HEXа: оригинальный, и полученный после наложения на него бинарного патча. Можно взять текстовый diff между ними:
> fc.exe /l /n 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' patch.hex Comparing files C:\PROGRAM FILES (X86)\KAT GATEWAY\katvr_foot.hex and PATCH.HEX ***** C:\PROGRAM FILES (X86)\KAT GATEWAY\katvr_foot.hex 2463: :2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2 2464: :2029E8005456520512089F000900020A039F1902AF9607B039043D4FF703636505771064CC 2465: :202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2 ***** PATCH.HEX 2463: :2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2 2464: :2029E800542D460512089F000900020A039F1902AF9607B039043D4FF70363650577106401 2465: :202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2 *****
Вот только как его наложить? Ну то есть diff есть (fc.exe) а вот patch нету! Что ж, так как формат патча прост и для нашего случая не требуется интеллектуального наложения (нужно чтобы он применился 1-в-1, это еще подтвердит что исходная прошивка правильная), то можно сделать просто: читаем файл, для каждой строки в исходном проверяем равенство, для каждой строки выходной просто печатаем её.
А можно вообще, превратить этот дифф в скрипт, который будет прямо патчить. Не знаю что проще, если честно, но я пошел вторым путём (если честно -- чтобы не возиться с чтением двух файлов, а просто работать как обработчик пайпа). Конвертер делается простой стейт машиной:
"Comparing files" строка => выводим заголовок, переходим в ожидание патча
"*****" в ожидании патча => переходим в ожидание строк исходного
"*****" в ожидании строк исходного => переходим в ожидание патченных строк
другая строка в ожидании строк исходного => печатаем if на номер строки и её содержимое
"*****" в ожидании патченных строк => переходим в ожидание патча
срока в ожидании патченных строк => печатаем её
Из важных вещей с которыми пришлось познакомиться: по умолчанию редирект работает не в той кодировке, что было на входе и не в utf8, а в utf-16. Так что приходится редирект отсылать в Out-File -Encoding Ascii. Но это не сильно мешает.
Итак, теперь можно скормить ему патч, и посмотреть, что получилось:
> fc.exe /l /n 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' patch.hex | .\fc-text-to-patcher.ps1 | Out-File -Filepath foot-patch.ps1 -Encoding Ascii > cat .\foot-patch.ps1 $in_line = 0 $Input | ForEach-Object { $in_line++ $skip = 0 if ($in_line -eq 2463) { if ($_ -ne ':2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2') { throw 'File content mismatch'; } $skip = 1 } if ($in_line -eq 2464) { if ($_ -ne ':2029E8005456520512089F000900020A039F1902AF9607B039043D4FF703636505771064CC') { throw 'File content mismatch'; } $skip = 1 } if ($in_line -eq 2465) { if ($_ -ne ':202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2') { throw 'File content mismatch'; } $skip = 1 } if ($in_line -eq 2465) { Write-Output ':2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2' } if ($in_line -eq 2465) { Write-Output ':2029E800542D460512089F000900020A039F1902AF9607B039043D4FF70363650577106401' } if ($in_line -eq 2465) { Write-Output ':202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2' } if ($skip -eq 0) { Write-Output $_ } }
Отлично! Проверим:
> cat 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' | .\foot-patch.ps1 | Out-File -Filepath out.hex -Encoding ascii > fc.exe .\out.hex .\patch.hex Comparing files .\out.hex and .\PATCH.HEX FC: no differences encountered
Последние штрихи
Поправим скрипт, добавим вызов восстановления и код для наложения патча. Поймём, что в папке scripts образовалась каша, так что... Время рефакторинга! оставляем нормальные имена файлов для .cmd -- для пользователей, чтоб в начале были.
Оба рабочих скрипта (прошивки и восстановления настроек) перенесём в конец, обозвав их y-$script.ps1. А патчи положим в z_patch_$sensor.ps1.
Еще заметка на полях:
Вызвать скрипт косвенно через переменную -- амперсанд. Ну то есть наложение патча идёт через:
$newfw = $ENV:TEMP + "\katvr_" + $orig + "_patch.hex" $patchscript = ".\z_patch_" + $patch + ".ps1" Get-Content $firmware | & $patchscript | Out-File -FilePath $newfw -Encoding ascii
Полученную кашицу уже можно считать финалом.
В следующей серии
Теперь, когда мы подготовили материальную базу, можно заняться самой интересной частью -- разбором паршивок и правкой их по-настоящему, а не просто прямой правкой констант. Не переключайтесь!
Ссылки
Часть 1: "Играем с платформой" на [Habr], [Medium] и [LinkedIn].
Часть 2: "Начинаем погружение" на [Habr], [Medium] и [LinkedIn].
Часть 3: "Отрезаем провод" на [Habr], [Medium] и [LinkedIn].
Часть 4: "Играемся с прошивкой" на [Habr], [Medium] и [LinkedIn].
Часть 5: "Оверклокинг и багфиксинг" на [Habr], [Medium] и [LinkedIn].