- BrainTools - https://www.braintools.ru -

Как запустить Super Mario Bros на Xiaomi Mi Band 4

Однажды во время уборки я нашёл свой старый фитнес-браслет, Xiaomi Mi Band 4. По сути, это носимый компьютер с батареей, экраном, сенсорами и Bluetooth. Можно ли что-нибудь на нём запустить? Теоретически да, но проблема в том, что я не специалист по взлому hardware и не пишу на C под ARM. Но, может быть, нынешнее поколение LLM поможет мне запустить на нём что-нибудь интересное? Например, эмулятор NES, на котором можно поиграть в Super Mario Bros с управлением через сенсорный экран и клавиатуру по Bluetooth? Для меня всё это звучит как научная фантастика. Наверное, на это ушёл бы год целенаправленной работы. Но попытаться я думаю стоит

Для начала найдём прошивку. К счастью, она есть в интернете [1].

Характеристики Xiaomi Mi Band 4:

Компонент

Характеристики

Основной процессор

Dialog/Renesas DA14697, Arm Cortex-M33 с FPU, до 96 МГц.

Bluetooth-процессор

Отдельное ядро Cortex-M0+ для контроллера, встроенный радиомодуль на 2.4 ГГц и подключение антенны. Браслет поддерживает Bluetooth 5.0 LE.

Сопроцессор сенсоров

Sensor Node Controller, SNC: может собирать данные сенсоров, пока основной CPU спит.

Оперативная память [2]

512 KiB SRAM

Кеш инструкций

16 KiB, поддерживает выполнение кода прямо из внешней flash-памяти.

Внутренняя ROM / OTP

128 KiB ROM и 4 KiB однократно программируемой памяти.

Внешняя память

16 MiB serial NOR flash

Экран

0.95-дюймовый AMOLED, 120 × 240 RGB, регулируемая яркость.

Контроллер сенсорного экрана

Ёмкостный контроллер Zinitix

Передняя кнопка

Одна ёмкостная сенсорная кнопка для пробуждения и возврата назад.

Датчик определения ношения

Ёмкостное определение контакта с кожей; в прошивке обнаружен Azoteq IQS620.

Акселерометр

Три оси

Гироскоп

Три оси. Прошивка поддерживает варианты устройства с Bosch BMI260 или ST LSM6DSO.

Оптический датчик пульса

Зелёные светодиоды, фотодетектор и оптический analog front end TI AFE4900, найденный в прошивке.

Вибрация

ERM / роторный вибромотор

Батарея

Перезаряжаемый литий-полимерный аккумулятор на 135 мА·ч.

Версия нашей прошивки: 1.0.9.66.

Как с этим устройством общаться? Перед первым использованием браслет просит воспользоваться приложением Mi Fit (Zepp Life). Но зачем?

Дело в cloud pairing:

  1. Браслет отправляет приложению по Bluetooth случайное значение и отпечаток публичного ключа.

  2. Приложение и браслет вычисляют один и тот же ключ:

    D = SHA256(band_MAC_bytes || 16-byte_random) # 32 bytes
    auth_key = first 16 bytes of D
    
  3. Zepp Life просит облако подписать D. Приложение отправляет полный 32-байтовый хеш (D) и отпечаток публичного ключа, а в ответ получает подпись от сервера.

    Затем оно отправляет эту подпись браслету.

  4. Браслет проверяет подпись.

    В Ghidra видно, что 0x616c → 0x2fefc выполняет проверку ECDSA-secp256k1 с доверенным публичным ключом браслета:

    ECDSA_verify(trusted_public_key, D, signature)
    

    Если проверка прошла, можно продолжать.

  5. После подтверждения сопряжения браслет сохраняет ключ.

Далее при каждом подключении:

  1. Браслет отправляет новый случайный challenge – случайное значение длиной 16 байт.

  2. Приложение отвечает результатом AES-128-ECB(auth_key, challenge).

  3. Браслет вычисляет то же самое со своим сохранённым ключом. Если результаты совпадают, он разрешает доступ к защищённым операциям.

Если у вас есть Android-телефон с root (как у меня), после cloud pairing этот ключ можно извлечь из приложения. Он хранится в /data/data/com.xiaomi.hm.health/databases/origin_db_*.

Вот что можно делать с auth_key:

Операция по BLE

Нужен auth_key?

Найти браслет, подключиться, получить список сервисов

Нет

Прочитать серийный номер и версии устройства/прошивки

Нет

Прочитать информацию о батарее

Нет

Прочитать текущее время на браслете

Нет

Прочитать текущее количество шагов

Да

Запустить или остановить измерение пульса и получать результаты

Да

Запустить или остановить передачу сырых данных сенсоров

Да

Скачать сохранённую историю активности и показателей здоровья

Да

Изменить персональные настройки, будильники, цели, единицы измерения, DND, настройки экрана и запястья

Да

Установить дату и время

Да

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

Да

Включить вибрацию / найти браслет

Да

Отправить погоду и прогноз

Да

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

Да

Управлять состоянием тренировки/сессии

Да

Загрузить прошивку, циферблаты, ресурсы или шрифты

Да

Скачанная прошивка подписана, и при обновлении эта подпись проверяется браслетом. Конечно, можно вскрыть устройство и прошить чипы памяти напрямую, но аккуратно разобрать его не получится, оно водонепроницаемое. Но можно попробовать сделать всё по Bluetooth. Если найдётся способ удалённо выполнить свой код, то мы сможем пропатчить прошивку изнутри. Отключим проверку подписи и будем свободно устанавливать изменённые прошивки через штатный механизм обновления. Мечты…

Или это всё-таки возможно? Прошивка подписана, но не зашифрована. Значит, её можно декомпилировать в Ghidra. Я попросил Codex найти все места в прошивке, которые обрабатывают входные данные из Bluetooth. Самые интересные находки были связаны с загрузкой прошивок, ресурсов, шрифтов и циферблатов.

Что загружаем

Что внутри

Что меняется после обновления

Тип обновления

Прошивка (.fw)

Исполняемый код: обработка Bluetooth, аутентификация, сенсоры, меню, логика [3] отрисовки

Поведение [4] браслета; версия прошивки

0x00

Циферблат (.bin)

Оформление экрана часов: изображения, цифры и их расположение

Вид установленного циферблата

0x08

Ресурсы (.res)

Общие графические ресурсы интерфейса: иконки и изображения для экранов

Графика в интерфейсе браслета

0x02 raw; 0x82 compressed

Шрифты (.ft)

Глифы символов для отрисовки текста

Доступные символы и вид текста

0x01

Но на какие из этих данных мы можем влиять? Приватного ключа у нас нет, он где-то на серверах.

Что загружаем

Браслет проверяет подпись?

Проверка файла

Прошивка (0x00)

Да, ECDSA

Проверки trailer/header HMF и проверка подписи

Циферблат (0x08)

Нет

CRC32 сравнивается со значением, переданным отправителем

Ресурсы (0x02, compressed 0x82)

Нет

CRC32 сравнивается со значением, переданным отправителем

Шрифты (0x01)

Нет

CRC32 сравнивается со значением, переданным отправителем

Я попросил Grok покопаться в этих местах (так как codex отказался), и через некоторое время был найден первый баг. Ресурсы сжаты, а в коде их распаковки есть ошибка [5]: он может писать за пределы выделенного буфера и перезаписывать другие области RAM.

Как это может дать нам выполнение кода?

  1. Загружаем специально подготовленное обновление ресурсов.

  2. Заставляем декомпрессор писать за пределы буфера.

  3. Продолжаем запись, пока не подменим указатель, который использует установщик.

  4. Когда распаковка заканчивается, установщик использует этот указатель для вызова функции.

  5. Подменённый указатель направляет выполнение на наш код.

Мы попробовали это сделать. Сначала браслет отключался через несколько секунд после отправки обновления. Он падал и перезагружался, и было непонятно, что именно там происходит. К счастью, в штатной прошивке нашёлся полезный диагностический канал: внутренний debug log браслета, который можно скачать по Bluetooth. В прошивке 1.0.9.66 он доступен как Huami recorded-data type 7 и хранится в кольцевом буфере размером примерно 1 MiB. Логи хранятся как обычный читаемый текст: сообщения о загрузке, батарее и сенсорах, события Bluetooth, ход обновления и отчёты о сбоях.

Например, вот фрагмент лога:

[43] hrm_init, mode:0, intv:0
[43] App reboot time 9/9 13:23:58 1.0.9.66
[45] In check_free_region notSynced region=2000->e2000
[45] not synced region=2000->e2000
[45] store_manager_init ==> multi_link is 0
[45] flash id:0xef8018
[45] ==>reset hardfault
[45] R0: 0x2007d242
[45] R1: 0x200210e9
[45] R2: 0x80009801
[45] R3: 0x1
[45] R12: 0x2007d23a
[45] LR: 0xbe2a1
[45] PC: 0xbe286
[45] PSR: 0x1000014
[45] ==>reset hardfault
[45] task: APP_
[45] debug task state:0x0

Получив crash logs, мы смогли понять, что происходило на самом деле:

  1. Первое переполнение зашло слишком далеко: запись продолжалась до конца RAM, после чего браслет падал. Кроме того, адрес буфера менялся между перезагрузками, поэтому одинаковая длина переполнения не всегда приводила нас к одному и тому же адресу.

  2. Мы сократили запись. Теперь сбои вызывал радиопроцессор браслета: его код и данные находились между нашим буфером и указателем, который мы хотели подменить.

  3. В конце концов мы перезаписали нужный указатель. Выглядело многообещающе, но браслет падал до завершения распаковки, так что установщик не успевал его использовать.

  4. Мы остановили переполнение перед памятью радиопроцессора. Распаковка завершилась, но нужный указатель остался прежним. Установщик вызвал обычную функцию стирания и стёр часть ресурсов.

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

Память была устроена так:

Наш буфер распаковки
         ↓
Код и данные радиопроцессора
         ↓
Указатель, который мы хотели перезаписать

Если мы записывали достаточно данных, чтобы добраться до указателя, то по дороге уничтожали код и данные радиопроцессора. Если останавливались достаточно рано, чтобы их не затронуть, то до указателя не доходили.

Потратив на этот подход некоторое время, я решил поискать другой путь.

Прошло какое-то время, и мне пришла в голову мысль: мы видели немало уязвимостей в Android, iOS и других системах, где специально подготовленное изображение вызывало ошибку в коде обработки картинок. Это могло приводить к повреждению памяти и выполнению кода. Может быть, на этом устройстве получится что-то похожее?

После неудачи со сжатыми ресурсами мы попробовали другой вектор: свой циферблат, загружаемый как тип 8 (HMDIAL). Покопавшись в прошивке, нашли ещё один баг, теперь в коде чтения BMP.

Этот код работает так:

  1. Читает одну строку BMP шириной 120 пикселей в небольшой буфер.

  2. Преобразует пиксели в цветовой формат экрана и рисует строку.

  3. Переиспользует буфер для следующей строки.

В буфере 360 байт, как раз на 120 пикселей по три байта каждый.

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

А что будет, если загрузить 32-битный BMP? Теперь на пиксель приходится четыре байта, и для строки нужно 480 байт. Декодер всё равно копирует строку целиком в буфер на 360 байт. Лишние 120 байт перезаписывают память после буфера, в том числе указатель на список callback-функций.

Мы помещаем свой код и свой список callback-функций в пиксельные данные BMP. Когда декодер копирует пиксели в RAM, туда попадает и наш код. Переполнение подменяет исходный указатель адресом нашего списка. Когда прошивка в следующий раз использует этот список для обработки события, она вызывает наш код.

Есть одна деталь: встроенные и загруженные циферблаты обрабатываются по-разному. Браслет поддерживает один загруженный циферблат, которому соответствует значение 0xfe. Для его отрисовки и обновления используется отдельная запись с callback-функциями, которую мы не меняли. А вот события кнопки и вход на экран циферблата по-прежнему проходят через перезаписанный указатель. Поэтому эти события могут вызвать наш код.

Наш план:

  1. Поместить наши функции, список из трёх элементов и одну структуру с callback-функциями в пиксели BMP. В каждой строке будут одни и те же байты

  2. Дать штатному декодеру скопировать каждую строку в буфер с фиксированным адресом.

  3. Строка 32-битного BMP длиной 480 байт перезапишет кешированный указатель на список сразу после 360-байтового буфера и направит его на наш список.

  4. Последующие обращения пойдут через этот указатель и вызовут наши функции.

Специально подготовленный BMP-циферблат на браслете

Специально подготовленный BMP-циферблат на браслете

Чтобы проверить, выполняется ли наш код, мы использовали функцию, которая отключала прерывания, некоторое время крутилась в цикле со счётчиком, а затем возвращала управление. После того как загруженный циферблат хотя бы раз появлялся на экране, нажатие Home заставляло браслет зависнуть примерно на 30 секунд. Затем он оживал, а повторное нажатие снова вызывало зависание.

После этого мы тем же способом вывели на экран разные адреса из прошивки, чтобы убедиться, что всё действительно работает. И оно работало. У нас был RCE.

Адреса из прошивки, выведенные на экран браслета

Адреса из прошивки, выведенные на экран браслета

Отключаем проверку прошивки

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

Браслет хранит во flash два образа прошивки: A и B. Заголовок с настройками загрузки (Pp) указывает, какой из них активен и какой был обновлён последним. Когда вы нажимаете кнопку обновления в мобильном приложении, оно отправляет прошивку в неактивный слот браслета, затем даёт команду переключить указатель в заголовке на новую прошивку и перезагрузиться. Мы использовали RCE, чтобы изменить прошивку в неактивном слоте: отключили проверку подписи, заставив её всегда возвращать успех.

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

Затем мы переключили загрузку на пропатченную прошивку в слоте A и перезагрузились.

С этой прошивкой обычный Bluetooth-updater уже принимал изменённые образы. Через него мы установили ещё одну пропатченную копию в слот B и снова перезагрузились.

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

Свои приложения

В существующей прошивке уже есть операционная система FreeRTOS, графическая библиотека emWin и драйверы железа. Мы хотим сделать свой hello world, который мог бы показывать, например, объём свободной RAM. Но как вообще добавить приложение в бинарный образ прошивки?

В Ghidra мы нашли нужные функции и разобрались, как их вызывает штатная прошивка. Например, зная адрес функции отрисовки текста и её аргументы, можно вызвать её через указатель на функцию в C. Передаём строку и координаты, а рисует уже существующая графическая библиотека.

Мы написали приложение на C, скомпилировали его под ARM-процессор браслета и добавили машинный код в конец образа прошивки. Linker script назначил адреса нашему коду и константам. Исходная прошивка осталась по прежним адресам, поэтому все её внутренние ссылки продолжили работать. Затем мы обновили поля длины и контрольную сумму образа и прошили его.

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

Пункт NES в меню браслета

Пункт NES в меню браслета

Эмулятор NES

Теперь, когда мы умеем запускать приложения, можно взяться за эмулятор NES. За основу взяли open-source эмулятор agnes [6]. При первом запуске получили около 5 FPS в Pong, что конечно печально.

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

И кажется, что это идеальная задача для LLM: можно создать среду, где изменения проверяются на настоящем железе и попытаться ее забенчмаксить.

  1. Мы добавили метрики: FPS, время отрисовки и передачи кадров (на дисплей), такты CPU, свободную RAM. Сохраняем их на устройстве и передаём обратно на ПК

  2. Добавили детерминированную последовательность нажатий и измеряем на ней метрики. Это будет наш benchmark.

  3. Чтобы убедиться, что результат не изменился, мы собрали набор эталонных кадров из исходного эмулятора agnes. Считаем хеши кадров и сравниваем их.

  4. Ведём журнал экспериментов, чтобы видеть, что сработало, а что нет.

  5. Цикл оптимизации полностью автономен: нам не нужно за ним следить.

Сам цикл эксперимента выглядит так:

  1. Проверяем производительность изменения и профилируем его на ПК. Мы построили частичную среду выполнения на основе Unicorn [7] (это фреймворк для эмуляции CPU). Она выполняла наши скомпилированные ARM-инструкции, а отдельные интерфейсы штатной прошивки и железа были смоделированы на Python. Мы проверяли результаты и собирали профили выполнения инструкций: какие функции и блоки кода выполняли больше всего работы.

  2. Если изменение выглядит многообещающе, собираем новую прошивку и обновляем Mi Band. Позже мы добавили Bluetooth-загрузчик нативного ARM-кода и хуки, через которые этот код мог заменять отдельные функции эмулятора. Теперь многие оптимизации можно было проверять без перепрошивки браслета.

  3. Запускаем на настоящем устройстве и получаем диагностические данные с железа.

  4. Затем либо добавляем изменение к уже принятым оптимизациям, либо отбрасываем его, либо пробуем доработать.

Вот несколько интересных оптимизаций, которые удалось найти.

Отключаем синхронизацию по TE

После отрисовки кадра мы вызывали штатную функцию вывода на экран. Изображение лежало во framebuffer в RAM: 120 х 240 пикселей по два байта, то есть 57 600 байт на кадр. Контроллер дисплея уже использовал DMA: железо само читало эти байты из RAM и передавало их на экран, без копирования каждого пикселя силами CPU.

Но передача начиналась не сразу. Штатный драйвер ждал от дисплей панели сигнал TE, tearing effect. Изображение на экране обновляется построчно. Если передать новый кадр в середине этого процесса, часть экрана может показывать старый кадр, а часть уже новый. Это называется tearing. Сигнал TE даёт драйверу ориентир по времени, чтобы согласовать передачу с обновлением панели.

Драйвер уже умел отправлять кадры без синхронизации по TE. Мы нашли настройку в его конфигурационной структуре: байт по смещению 0x3d. Скопировали структуру, обнулили этот байт в своей копии и передали её существующему драйверу. Теперь передача начиналась сразу. Перед следующей записью во framebuffer мы всё ещё ждали завершения DMA, иначе контроллер мог читать пиксели одновременно с тем, как мы их перезаписывали.

В контролируемом сравнении FPS среднее время вывода кадра уменьшилось с 24.93 до 10.10 мс, а время отрисовки не изменилось (как и задумано). В двух сценах Mario частота кадров на экране выросла примерно с 19.4 FPS до 27.02 и 25.09 FPS.

Компромисс был в том, что передача теперь могла попасть на менее удачный момент обновления панели. Но во время игры я этого не заметил, поэтому мы оставили изменение.

Горячий код в RAM

Значительная часть кода штатной прошивки выполняется прямо из flash через XIP [8]. Мы профилировали Mario, нашли самые часто используемые 6502 opcodes [9] и поместили код их эмуляции прямо в SRAM.

Обработчики инструкций CPU в SRAM

Сцена 240

Сцена 480

0 — CPU-интерпретатор работает из flash

30.12 FPS

27.72 FPS

48 — часто используемые обработчики работают из SRAM

32.06 FPS

29.74 FPS

Прирост

6.5%

7.3%

Как получить больше RAM?

Мы добавили game boot mode, в котором браслет запускается с меньшим количеством штатных сервисов. При входе в этот режим он перезагружается и идёт по изменённому пути старта, пропуская задачи сенсоров, калибровки и синхронизации показателей здоровья и другие. Если не инициализировать эти задачи, память, которую они бы выделили, остаётся эмулятору. Это позволило получить 84 568 свободных байт после запуска эмулятора. Чтобы вернуться к обычному режиму, достаточно нажать Home два раза.

Double buffering

Штатный вывод на экран уже использовал DMA, поэтому CPU не приходилось копировать каждый байт. Но с одним framebuffer он всё равно должен был ждать, прежде чем менять память, которую в этот момент читает контроллер дисплея.

Можно использовать double buffering [10]. Пока DMA отправлял кадр N из одного буфера, CPU мог эмулировать и рисовать кадр N+1 в другом. В идеале время на кадр меняется с суммы времени отрисовки и передачи на max(время отрисовки, время передачи), плюс накладные расходы на планирование и подготовку.

Каждый framebuffer размером 120 x 240 в формате RGB565 занимает 57 600 байт. Это одна из причин, по которым нам понадобилось больше RAM. Со вторым буфером CPU48 вырос с 32.06 / 29.78 до 46.68 / 42.10 FPS – примерно на 45% в измеренных сценах.

Прямое чтение инструкций из ROM

В адресном пространстве NES выделены отдельные области для RAM, памяти картриджа и аппаратных регистров. При каждом чтении байта эмулятор проверял, к какой области относится адрес. Эта проверка выполнялась даже при чтении инструкций игры.

Но ROM уже загружен в RAM браслета. Мы могли один раз найти начало текущего блока ROM размером 16 KiB, а затем читать каждую инструкцию прямо из этой памяти. Так не приходилось повторять [11] одни и те же проверки адреса для каждого байта. Возле конца блока или при выполнении кода из RAM самой NES мы возвращались к полному интерпретатору.

Это повысило FPS примерно на 1% и 5% в двух сценах.

Для трёхбайтовой инструкции, где address — её адрес в памяти NES:

# Было: определяем, к чему относится каждый адрес NES
byte1 = read_nes_memory(address)
byte2 = read_nes_memory(address + 1)
byte3 = read_nes_memory(address + 2)

# Стало: находим блок ROM один раз для группы инструкций
chunk, start_address = find_rom_chunk(address)

# Затем читаем каждую инструкцию прямо из этого блока
offset = address - start_address
byte1 = chunk[offset]
byte2 = chunk[offset + 1]
byte3 = chunk[offset + 2]

Результаты оптимизации

Титульный экран Super Mario Bros на браслете

Титульный экран Super Mario Bros на браслете

Этап оптимизации

Сцена Mario после 240 прогревочных кадров

Сцена после 480 прогревочных кадров

CPU48, синхронный вывод на экран

32.06 FPS

29.78 FPS

CPU48, double buffering

46.68 FPS

42.10 FPS

Выполнение инструкций CPU группами

51.20 FPS

46.14 FPS

Прямое чтение инструкций из ROM

51.70 FPS

48.84 FPS

CPU64

55.50 FPS

51.53 FPS

Объединение вывода PPU и преобразования цветов

57.09 FPS

53.08 FPS

Управление с клавиатуры и библиотека игр

Мы сделали приложение на Python, которое отправляет нажатия клавиш браслету по Bluetooth. Оно сообщает эмулятору, какие кнопки NES сейчас зажаты, а эмулятор читает их состояние перед выполнением кадра.

Во время разработки ROM и код нашего ускоренного эмулятора находились в RAM и пропадали после перезагрузки. Мы нашли неиспользуемую область flash размером 512 KiB и стали хранить их там. В ней помещается восемь игр.

Теперь можно один раз загрузить игру, открыть More → NES, выбрать её и нажать PLAY. Браслет запоминает выбор, перезагружается в game boot mode, загружает игру и код эмулятора из flash в RAM и начинает игру. Двойное нажатие Home возвращает обычную загрузку и восстанавливает штатные сервисы.

Управление с клавиатуры в Mi Band NES Controller

Управление с клавиатуры в Mi Band NES Controller

Заключение

Кажется, что уже сейчас инструменты на базе LLM позволяют браться за очень амбициозные проекты и, что самое главное, за проекты с высокой степенью неопределённости. Особенно интересна с практической точки зрения [12] возможность взять под контроль собственное железо и приспособить его под свои задачи. Это могут быть браслеты, читалки и другие устройства. Мне очень импонируют идеи по типу Right to repair [13] и я думаю, что LLM позволяют нам стать настоящими владельцами наших устройств. Дальше я хочу попробовать портировать этот проект [14] и превратить Mi Band в персональный универсальный трекер с поддержкой сервисов Apple и Google. Кажется, это немного практичнее эмулятора NES, хотя и не так весело :D

Спасибо за чтение и вперёд, к новым хакам!

Репозиторий на GitHub [15]

Автор: qwertyforce

Источник [16]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/37020

URLs in this post:

[1] интернете: https://4pda.to/forum/index.php?showtopic=956566

[2] память: http://www.braintools.ru/article/4140

[3] логика: http://www.braintools.ru/article/7640

[4] Поведение: http://www.braintools.ru/article/9372

[5] ошибка: http://www.braintools.ru/article/4192

[6] agnes: https://github.com/kgabis/agnes

[7] Unicorn: https://github.com/unicorn-engine/unicorn

[8] XIP: https://en.wikipedia.org/wiki/Execute_in_place

[9] 6502 opcodes: https://www.nesdev.org/wiki/Instruction_reference

[10] double buffering: https://en.wikipedia.org/wiki/Multiple_buffering

[11] повторять: http://www.braintools.ru/article/4012

[12] зрения: http://www.braintools.ru/article/6238

[13] Right to repair: https://en.wikipedia.org/wiki/Right_to_repair

[14] этот проект: https://github.com/vasimv/Everytag

[15] Репозиторий на GitHub: https://github.com/qwertyforce/miband4_nes

[16] Источник: https://habr.com/ru/articles/1094550/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1094550

www.BrainTools.ru

Rambler's Top100