Всем привет, меня зовут Алексей. Я много лет занимаюсь тюнингом часов Xiaomi. Мне нравится эти гаджеты прежде всего тем, что они подвластны глубокой модификации, несмотря на наличии подписей и прочих защит, которую зачастую не могут или не хотят нормально организовать.
Совершенно случайно ко мне попал на стол сегодняшний пациент Mibro Watch Lite3, довольно симпатишные часы, позиционируют себя как суббренд Сяоми, но по факту не очень понятно что это. Mibro, Haylou и подобные, обладают существенным недостатком - плохо проработанным и локализованном софте как в телефонах, так и в часах, плюс отсутствием поддержки делать какие либо пользовательские циферблаты.
Мне написал владелец Mibro Watch GS Pro модели и попросил помочь разобраться как работать с циферблатами этой модели, циферблат оказался обычным .zip архивом со своим форматом графики и ВНИМАНИЕ! .elf файлом циферблата.
Не люблю такие модели, ибо там все грустно и неинтересно, но тут я глянул, что внутри,
и о майн гад, там нативный UI lvgl код, бегом в магаз - за недорогую Amoled версию данного бренда и начинаем ковырять это чудо!
Обзор кухни данных устройств
Приложение для данных часов называется Mibro Fit, в общем ничего выдающегося, но пожалуй и откровенно плохого. Главное что нам далее понадобится - оно кеширует циферблаты с онлайн магазина в телефоне и не проверяет хеш, чтобы потом плюнуть их в часы ;)
Интерфейс Mibro Fit

System-On-Chip (SoC), используемый тут - SF32LB551, китайского производителя Sifli Technology,
ARM Cortex-M33 240Mhz, 2.5 GPU, BLE 5.3
Состав SoC SF32LB55x

Как видно, тут есть и граф ускоритель, еще аппаратный блок ezip, формат которого и используется в графике часов.
Данный SoC используется многими китайскими производителями часов (у Xiaomi это дешевые, знакомые нам Redmi Watch 5 Active/Lite и Redmi Watch 4 Move), так как по сути представляет себя хороший интеграционный пакет с готовыми примерами. И что выгодно отличает от других китайских производителей, довольно неплохой документацией и SDK который доступен на github SiFli Documentation
Система, RTOS RT-Thread, активно использующаяся китайскими производителями, поинтереснее FreeRTOS, но попроще чем NuttX, которую юзает Xiaomi.
Графическая подсистема lvgl, очень популярная и отлично документированная, с большим комьюнити. Разрешение Watch Lite3 - 360x360, в модели Watch GS Pro 466x466.
Устройство циферблата
Как я упоминал ранее, циферблат представляет собой zip архив, со следующей структурой.

Особый интерес тут представляют файлы графики в папке ezip, а также *.so - это те самые исполняемые .elf файлы циферблата, wf_50231.so - основное приложение, и _res.so - какие-то дополнительные ресурсы, в данном циферблате не используется,
wf_50231_tn.bin - ezip превью циферблата
Меняем графику
Компания SiFli в своем SDK имеет специальную утилиту для работы с графикой, это ezip.exe.
Данный формат я уже разбирал самостоятельно, это DEFLATE, в данном случае оптимизированный под аппаратный декодер, если знакомы с алгоритмом сжатия DEFLATE, то тут дерево Хаффмана выделено в общую часть, а данные разбиты на блоки, чтобы вписать в размер аппаратного буфера.
Итак, берем SDK утилиту ezip и конвертируем файл .ezip в .png
ezip -convert wf_50231_dynamic_energy_bg.bin -spt 1 -dpt 0 -outdir .

Конвертируем обратно в .ezip
ezip -convert wf_50231_dynamic_energy_bg_ru.png -binfile 2 -outdir .
Упаковываем обратно в архив, подкидываем циферблат в приложение,
устанавливаем его в часы, и.. установка завершается с ошибкой.
Внимательнее смотрим в бинарник картинки, и находим там magic number.

Красным выделен размер данных картинки, он в big-endian, a перед ним типичный заголовок lvgl image.

Проверяем размер и обнаруживаем лишние 4ре байта, это явно чексумма,
проверяем другие картинки, тоже самое.
Спустя некоторое время для подбора алгоритма crc32, выясняю,
что чек-сумма соответствует CRC-32/MPEG-2
Ок, пишу небольшой код для исправления/добавления чексуммы (в репо в линках) и получаю готовую картинку с правильным crc.
Гружу новый циферблат - все замечательно встало в часах.

Меняем приложение циферблата
Теперь к более интересной части. Код циферблата.
Согласно документации RT-Thread, данная ось поддерживает динамическую загрузку программ
В нашем случае - это .so - динамическая библиотека.
Заглянем внутрь и посмотрим как устроено библиотека циферблата.
entry point циферблата - module_init - module_cleanup

Функция регистрации циферблата


интерфейс циферблата
состоит из 4 методов
Функция face_create()
int __fastcall face_create(int *root, int prm2, int prm3)
{
int user_data; // r0
const char *week_style_convert_str; // r0
_BYTE *current_time; // [sp+1Ch] [bp-54h]
unsigned __int8 *rt_data; // [sp+3Ch] [bp-34h]
dlmodule_get_user_data("wf_50231", 64);
user_data = dlmodule_get_user_data("wf_50231", 64);
memset(user_data, 0, 64);
current_time = (_BYTE *)service_get_current_time(0);
*(_BYTE *)dlmodule_get_user_data("wf_50231", 64) = current_time[4];
*(_BYTE *)(dlmodule_get_user_data("wf_50231", 64) + 1) = current_time[5];
*(_BYTE *)(dlmodule_get_user_data("wf_50231", 64) + 3) = current_time[3];
*(_BYTE *)(dlmodule_get_user_data("wf_50231", 64) + 2) = current_time[10];
v5 = dlmodule_get_user_data("wf_50231", 64);
*(_BYTE *)(v5 + 4) = mbr_config_get_time_format();
parent = *root;
// background image
v7 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v7 + 8) = lv_img_create(parent, 0);
lv_img_set_auto_size(*(_DWORD *)(v8 + 8), 1);
lv_img_set_src(*(_DWORD *)(v9 + 8), "/dyn/dynamic_app/watchface/wf_50231/ezip/wf_50231_dynamic_energy_bg.bin");
lv_obj_align(*(_DWORD *)(v10 + 8), parent, 0, 0, 0);
lv_obj_set_click(*(_DWORD *)(v11 + 8), 1);
lv_page_glue_obj(*(_DWORD *)(v12 + 8), 1);
lv_obj_set_parent_event(*(_DWORD *)(v13 + 8), 1);
// label Время
v14 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
v15 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v15 + 12) = lv_label_create(v14, 0);
lv_obj_set_style_local_color(*(_DWORD *)(v16 + 12), 0, 0x8089, 0xFFFF);
lv_obj_set_click(*(_DWORD *)(v17 + 12), 0);
lv_obj_set_style_local_ptr(*(_DWORD *)(v18 + 12), 0, 0x808E);
v19 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 12);
v20 = mbr_utils_convert_time_hour_format((unsigned __int8)current_time[4]);
lv_label_set_text_fmt(v19, "%02d:%02d", v20, (unsigned __int8)current_time[5]);
lv_obj_set_auto_realign(*(_DWORD *)(v21 + 12), 1);
v22 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 12);
v23 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_align(v22, *(_DWORD *)(v23 + 8), 0, 0, 80);
v136 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
v24 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v24 + 16) = lv_label_create(v136, 0);
v25 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_style_local_color(*(_DWORD *)(v25 + 16), 0, 0x8089, 0xFFFF);
v26 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_click(*(_DWORD *)(v26 + 16), 0);
v27 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_style_local_ptr(*(_DWORD *)(v27 + 16), 0, 0x808E);
v28 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 16);
v29 = dlmodule_get_user_data("wf_50231", 64);
week_style_convert_str = (const char *)mbr_gui_utils_get_week_style_convert_str(*(unsigned __int8 *)(v29 + 2), 1);
lv_label_set_text_fmt(v28, "%s", week_style_convert_str);
v31 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_auto_realign(*(_DWORD *)(v31 + 16), 1);
v32 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 16);
v33 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_align(v32, *(_DWORD *)(v33 + 8), 0, 0, 120);
v137 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
v34 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v34 + 20) = lv_label_create(v137, 0);
v35 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_style_local_color(*(_DWORD *)(v35 + 20), 0, 0x8089, 0xFFFF);
v36 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_click(*(_DWORD *)(v36 + 20), 0);
v37 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_style_local_ptr(*(_DWORD *)(v37 + 20), 0, 0x808E);
v38 = dlmodule_get_user_data("wf_50231", 64);
lv_label_set_text_fmt(*(_DWORD *)(v38 + 20), "%d", (unsigned __int8)current_time[3]);
v39 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_auto_realign(*(_DWORD *)(v39 + 20), 1);
v40 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 20);
v41 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_align(v40, *(_DWORD *)(v41 + 8), 0, 0, 160);
rt_data = (unsigned __int8 *)app_db_get_rt_data(1);
v138 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
v135 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v135 + 24) = lv_label_create(v138, 0);
lv_obj_set_style_local_ptr(*(_DWORD *)(v42 + 24), 0, 0x808E);
lv_obj_set_style_local_color(*(_DWORD *)(v43 + 24), 0, 0x8089, 0xFFFF);
lv_obj_set_click(*(_DWORD *)(v44 + 24), 0);
lv_label_set_long_mode(*(_DWORD *)(v45 + 24), 3);
lv_label_set_align(*(_DWORD *)(v46 + 24), 1);
lv_obj_set_width(*(_DWORD *)(v47 + 24), 55);
if ( *rt_data )
{
v48 = dlmodule_get_user_data("wf_50231", 64);
lv_label_set_text_fmt(*(_DWORD *)(v48 + 24), "%d", *rt_data);
}
else
{
v132 = dlmodule_get_user_data("wf_50231", 64);
lv_label_set_text_fmt(*(_DWORD *)(v132 + 24), "- -");
}
v49 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_auto_realign(*(_DWORD *)(v49 + 24), 1);
v50 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 24);
v51 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_align(v50, *(_DWORD *)(v51 + 8), 0, 0, 20);
v52 = (_DWORD *)app_db_get_rt_data(0);
v53 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
v54 = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(v54 + 28) = lv_label_create(v53, 0);
v55 = dlmodule_get_user_data("wf_50231", 64);
((void (__fastcall *)(_DWORD, _DWORD, int, void **))lv_obj_set_style_local_ptr)(
*(_DWORD *)(v55 + 28),
0,
0x808E,
&dynamic_energy_font_misc);
v56 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_set_style_local_color(*(_DWORD *)(v56 + 28), 0, 0x8089, 0xFFFF);
lv_obj_set_click(*(_DWORD *)(v57 + 28), 0);
lv_label_set_long_mode(*(_DWORD *)(v58 + 28), 3);
lv_label_set_text_fmt(*(_DWORD *)(v59 + 28), "%d", *v52);
lv_label_set_align(*(_DWORD *)(v60 + 28), 1);
lv_obj_set_width(*(_DWORD *)(v61 + 28), 98);
lv_obj_set_auto_realign(*(_DWORD *)(v62 + 28), 1);
v63 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 28);
lv_obj_align(v63, *(_DWORD *)(v64 + 8), 0, 0, -66);
v141 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
...
// кнопка Music
parent = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(userData + 32) = lv_obj_create(parent, 0);
lv_obj_set_size(*(_DWORD *)(userData + 32), 60, 60);
lv_obj_set_style_local_color(*(_DWORD *)(userData + 32), 0, 41, 0);
lv_obj_set_style_local_opa(*(_DWORD *)(userData + 32), 0, 44);
lv_obj_set_parent_event(*(_DWORD *)(userData + 32), 1);
lv_obj_set_drag_parent(*(_DWORD *)(userData + 32), 1);
lv_obj_set_click(*(_DWORD *)(userData + 32), 1);
lv_obj_align(v96, *(_DWORD *)(v97 + 8), 0, -96, -90);
lv_obj_set_event_cb(*(_DWORD *)(userData + 32), run_app_music_cb);
v99 = *(_DWORD *)(dlmodule_get_user_data("wf_50231", 64) + 8);
// кнопка Settings
parent = dlmodule_get_user_data("wf_50231", 64);
*(_DWORD *)(userData + 36) = lv_obj_create(parent, 0);
lv_obj_set_size(*(_DWORD *)(userData + 36), 60, 60);
lv_obj_set_style_local_color(*(_DWORD *)(userData + 36), 0, 41, 0);
lv_obj_set_style_local_opa(*(_DWORD *)(userData + 36), 0, 44);
lv_obj_set_parent_event(*(_DWORD *)(userData + 36), 1);
lv_obj_set_drag_parent(*(_DWORD *)(userData + 36), 1);
lv_obj_set_click(*(_DWORD *)(userData + 36), 1);
lv_obj_align(v107, *(_DWORD *)(v108 + 8), 0, 96, -90);
lv_obj_set_event_cb(*(_DWORD *)(userData + 36), run_app_settings_cb);
...
v12
8 = dlmodule_get_user_data("wf_50231", 64);
lv_obj_data_subscribe(*(_DWORD *)(v128 + 8), 0xA203, data_update_cb);
lv_obj_data_subscribe(*(_DWORD *)(v129 + 8), 0xA202, data_update_cb);
lv_obj_data_subscribe(*(_DWORD *)(v130 + 8), 0xA201, data_update_cb);
return 0;
}
В функции face_create() происходит полной создание всех элементов интерфейса с помощью lvgl библиотеки и подпись на обновление данных.
Заметьте что некоторые элементы, такие как время, дни недели и прочие цифровые данные отрисовываются с помощью шрифтов формата lvgl, которые прямо встроены в дата секцию приложения. Это самый проблемный момент в модификации, сменить названия дней недели не получится просто, нужно менять этот код.
В данном циферблате мне приглянулась кнопка Settings - по ее нажатию открывается меню настроек, на циферблате не самая полезная вещь, а не сменить ли ее на что нить полезное.
Давайте еще раз посмотрим на кусок кода отвечающий на это.
*(_DWORD *)(userData + 36) = lv_obj_create(parent, 0);
lv_obj_set_size(*(_DWORD *)(userData + 36), 60, 60);
lv_obj_set_style_local_color(*(_DWORD *)(userData + 36), 0, 41, 0);
lv_obj_set_style_local_opa(*(_DWORD *)(userData + 36), 0, 44);
lv_obj_set_parent_event(*(_DWORD *)(userData + 36), 1);
lv_obj_set_drag_parent(*(_DWORD *)(userData + 36), 1);
lv_obj_set_click(*(_DWORD *)(userData + 36), 1);
lv_obj_set_event_cb(*(_DWORD *)(userData + 36), run_app_settings_cb);
тут идет установка callback функции run_app_settings_cb - которая вызывается при нажатии, глянем что там
int __fastcall run_app_settings_cb(int obj, int event)
{
int result; // r0
if (event == 6) // LV_EVENT_SINGLE_CLICKED
return gui_app_run("Setting");
return result;
}
Получается, есть некоторый список приложений который можно запускать спец функцией ядра gui_app_run(app_id), теперь пройдемся по прошивке и посмотрим что у нас еще найдется.
Список приложений, что можно запускать

Ок, Weather - погода, отличный вариант, попробуем изменить на него.

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

Перебиваем значение на Weather - подходит идеально вместо Setting, я изначально хотел установить Countdown - это таймер, так как часто пользуюсь им, но текст получается длинным, а для этого нужно либо релоцировать смещение другое место, что для теста более емкая задача, и пока не нужное лишнее время.
Ну и поскольку с картинками уже поймали грабли, проверяем есть ли у нашего файла циферблата чексумма.

Да, есть родная и в том же формате, отлично, правим и пробуем..

Итак, что в итоге получилось:
У нас есть возможность модифицировать графику циферблатов часов Mibro,
а также возможность менять код приложения, что дает на самом деле очень много возможностей при желании, да модификация .elf не так проста, и возможно, я не проверял, при ошибке можно получить бутлуп - тут уже на совести разработчиков - реализация обработки нештатных ситуаций.
Но если озадачится, покопаться в доках RT-Thread
можно сделать циферблаты игры, приложения и много чего интересного,
и в отличие от модных нынче js, lua, micropython и прочего - это нативный код,
который работает максимально шустро.
От такой реализации я в восторге, были бы еще SDK и инструменты от самого производителя Mibrо, думаю было бы очень интересно многим энтузиастам.
Материалы по теме
SiFli Documentation - https://wiki.sifli.com/en/docs/index.html
SiFli SDK - https://docs.sifli.com/projects/sdk/latest/en/sf32lb52x/index.html
RT-Thread - https://www.rt-thread.io/
RT-Thread dlmodule - https://www.rt-thread.io/document/site/programming-manual/dlmodule/dlmodule/RT-Thread dynamic apps - https://github.com/RT-Thread/rtthread-apps/
LVGL - https://lvgl.io/
Циферблат данной статье - https://github.com/m0tral/MibroWatchFace/
ну и в телеге мой канал, там я делаю интересные штуки с часами Сяоми