Для тех, кто только подключился -- я рассказываю про платформу для 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].