
Итак, сейчас у меня на руках последняя модель из линейки SmartBand Pro. А, это довольно хорошее железо: Bestechnic BES2700iMP Cortex-M33, 16Mb inner SPI flash, ~2.5Mb SRAM, 16Mb PSRAM, external SPI Nand Flash 512/256Mb.
Все это на базе хорошего софта RTOS NuttX, UI lvgl v9,
app engines: LUA, AiotJS(quickjs в девичестве) - Vue подобный JavaScript-фреймворк.
Lua - встроены в циферблаты, а на базе AiotJS работают приложения.
Проблема глобалки
В глобальных моделях Xiaomi менеджмент ведет себя очень глупо, вместо того,
чтобы нанять, ну хотя бы одного разработчика, и сделать десяток простых приложений
для международной аудитории, просто удаляют этот движок из часов для глобальных релизов.
Цель сегодняшней задачи
Используя доступные на борту инструменты написать и запустить простое приложение калькулятор.
Как по мне, которое давно должно быть в стандартном наборе приложений часов.
Подготовка
Когда я несколько лет назад смотрел на первые часы Сяоми с движком LUA, Redmi Watch 4, а особенность всей сегодняшней задачи, является то, что LUA является отправной точкой,
благодаря чему возможно это решение, из него можно вызвать os.execute(“”) в OS shell, я обратил внимание, что разработчики Longcheer (подрядчик Xiaomi), импортировали практически все приложения из базового набора NuttX.
И среди них интересны три команды ‘insmod’, ‘lsmod’, ‘rmmod’:
insmod — загружает и устанавливает модуль ОС в систему из файла по указанному пути, регистрируя его под заданным именем.
lsmod — выводит список всех загруженных в данный момент модулей ОС, включая их имена, размер секции кода (text) и размер секции данных (data)
rmmod — выгружает (удаляет) установленный модуль из системы по его имени.
Тогда я отложил эту задачу, так как не знал как правильно собрать модуль и реализовывал свои интеграции через прямые инжекции кода в firmware, но новое решение через модули дает следующие преимущества:
не нужно жестко привязывать адрес кода
не нужно выделять память под глобальные переменные, все работает автоматически
можно быстро проверить концепцию, без нарушения функционирования системы, и возможных фатальных последствий для устройства, требующих разборки устройств и подключения программатора.
можно загружать выборочно, что требуется в моменте
загрузка кода для дебага сильно быстрее, чем лить прошивку приложения.
В общем, гибкость сильно повышается и уменьшается время для получения готового приложения.
Загружаемые модули функционируют аналогично модулям ядра (LKM) в Linux. Они позволяют динамически добавлять код (например, драйверы устройств или новые функции) в работающую операционную систему без необходимости её полной пересборки и перезагрузки. Подробнее об модулях можно ознакомится в документации NuttX
insmod умеет загружать 2 типа модулей ELF и NXFLAT, в ELF установщик поддерживает ET_REL (перемещаемый объект) и ET_DYN (разделяемый объект). Их главное различие заключается в том, на каком этапе и кто связывает их код с реальными адресами в памяти. адреса. ET_REL - cтатический линковщик (ld) в процессе компиляции, ET-DYN - Динамический линковщик ядра при каждом старте программы.
я остановится на варианте ET_REL - он более простой для сборки приложения. Итак, на данном этапе, у нас есть два важных момента, как будем запускать приложение:
Используем LUA циферблат для деплоя(загрузки) .elf приложения в часы
Вызовем команду os.execute(“lsmod /data/mb10p_app.elf mb10p_app”) для запуска/истанцирования.
Устройство приложений подсистемы Xiaomi
Разработчики Longcheer разработали и написали довольно неплохой структурированный и продуманный код системы, с выделением подсистем часов в отдельные независимые и расширяемые модули, одно из них, и интересных нам - подсистема приложений.
Ключевые моменты этой подсистемы:
весь интерфейс часов разделен логически на отдельные приложения
для управления приложениями выделена отдельная сущность - менеджер приложений.
для запуска приложение должно быть зарегистрировано, и если необходимо - добавлено в launcher.
Вот так выглядит global vtable встроенных Xiaomi приложений одной и модели часов.
global vtable встроенных Xiaomi приложений
g_xiaomi_apps ├── register_app_launcher ├── register_app_system ├── register_app_home ├── register_app_watchface ├── register_app_setupwizard ├── register_app_easter_egg ├── register_app_control_center ├── register_app_notifications ├── register_app_carcontrol ├── register_app_check_tool ├── register_app_calendar ├── register_app_tourling ├── register_app_settings ├── register_app_alipay ├── register_app_timer ├── register_app_worldclock ├── register_app_recorder ├── register_app_chronograph ├── register_app_todolist ├── register_app_perpetual_calendar ├── register_app_compass ├── register_app_alarm ├── register_app_find_phone ├── register_app_flashlight ├── register_app_media ├── register_app_nfccard ├── register_app_weather ├── register_app_voice ├── register_app_mijia ├── register_app_share_device ├── register_app_interconnect ├── register_app_remote_camera ├── register_app_phone ├── register_app_contact ├── register_app_womenhealth ├── register_app_vitality ├── register_app_breath ├── register_app_pressure ├── register_app_oxygen ├── register_app_heartrate ├── register_app_sleep ├── register_app_training ├── register_app_course ├── register_app_record ├── register_app_sports ├── register_app_activities └── register_app_fusion_center
у приложений выделены интерфейсы, и они состоят
и двух главных структур miwear_app_t и miwear_page_t
typedef struct miwear_app_t { int proto_app; int field_4; char *name; char *icon; uint16_t app_id; uint16_t flags; int field_14; int field_18; char *(*app_get_name)(void); int (*on_uninstall)(void); int field_24; int field_28; int field_2C; int field_30; int field_34; int (*on_signal)(void); int field_3C; } miwear_app_t; typedef struct miwear_page_t { void *parent_descriptor; uint8_t _pad_4[0x0C]; void *page_name; uint16_t page_id; uint16_t app_id; uint16_t flags; uint8_t _pad_1a[0x02]; int32_t scheduler_deadline; int32_t scheduler_priority; uint32_t async_destroy_state; uint8_t lifecycle_state; uint8_t layer; uint8_t page_kind; uint8_t _pad_2b[0x01]; void *activity_context; void *root_object; void *on_signal; void *runtime_default_56; uint8_t _pad_3c[0x04]; void *registry_prev; void *registry_next; void *runtime_parent; void *on_create; void *on_resume; void *on_newdata; void *on_start; void *on_pause; void *on_stop; void *on_destroy; void *on_back; void *on_keyevent; void *get_scroll_obj; } miwear_page_t;
Менеджер приложений имеет следующий интерфейс:
struct packagemanager_api_s { int (*install)(miwear_app_t *app, miwear_page_t *pages[], int count); int (*recover)(miwear_app_t *app); int (*uninstall)(miwear_app_t *app); int (*unregister)(miwear_app_t *app); int (*update_notify)(miwear_app_t *app); void *(*widgets_add)(miwear_app_t *app, void *widgets, int count); void (*widgets_remove)(miwear_app_t *app); int (*set_hidden)(miwear_app_t *app, int hidden); int (*is_hidden)(miwear_app_t *app); };
минимальный код регистрации приложения выглядит вот так
int register_app_flashlight() { miwear_page_t *pages[1]; pages[0] = &flashlight_page_main; return p_app_install(&app_flashlight, pages, 1); }
Собственно это самая главная информация, которая нужна для реализации любого приложения в системе часов.
Реализация загрузочного модуля
Теперь приступаем непосредственно к написанию и сборки загружаемого модуля приложения.
/* * mb10p_hello.c * * Loadable NuttX userspace module for Xiaomi Mi Band 10 Pro (mb10pro). * Loaded at runtime via the NSH command: * * insmod /data/mb10p_hello.elf hello_module * * ============================================================================ */ #include <stdint.h> #include "nuttx/syslog.h" #include "platform/miwear_system.h" static int say_hello_module(void) { syslog(LOG_WARN, "[hello_module] hello"); return 0; } // ---- ELF ENTRY POINT --------------------------------------------------- * __attribute__((constructor)) int module_initialize(void) { syslog(LOG_WARN, "[hello_module] loaded"); say_hello_module(); return 0; /* OK; loader adds us to the module registry */ } __attribute__((destructor, used)) static void module_uninitialize(void) { syslog(LOG_WARN, "[hello_module] unloaded"); }
тут важный момент - помечаем конструктор и деструктор,
именно их дергает insmod, rmmod при установке удалении.
модуль делает системный вызов syslog - и пишет в лог, пока этого достаточно,
чтобы проверить работу загрузки модуля.
Сборка модуля
Очень важный этап, не просто написать код, а собрать его так, чтобы insmod разобрал .elf, загрузил в RAM, сделал релокацию адресов и истанцировал его.
В итоге должно получиться вот что
c:\Xiaomi\_app_native_code>arm-none-eabi-readelf -hS mb10p_hello.elf ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: ARM Version: 0x1 Entry point address: 0x1 Start of program headers: 0 (bytes into file) Start of section headers: 828 (bytes into file) Flags: 0x5000000, Version5 EABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 15 Section header string table index: 14 Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000030 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002a0 000018 08 I 12 1 4 [ 3] .rodata PROGBITS 00000030 000064 000043 01 AMS 0 0 1 [ 4] .data PROGBITS 00000073 0000a7 000000 00 WA 0 0 1 [ 5] .bss NOBITS 00000073 0000a7 000000 00 WA 0 0 1 [ 6] .init_array INIT_ARRAY 00000074 0000a8 000004 04 WA 0 0 4 [ 7] .rel.init_array REL 00000000 0002b8 000008 08 I 12 6 4 [ 8] .fini_array FINI_ARRAY 00000078 0000ac 000004 04 WA 0 0 4 [ 9] .rel.fini_array REL 00000000 0002c0 000008 08 I 12 8 4 [10] .comment PROGBITS 00000000 0000b0 00004e 01 MS 0 0 1 [11] .ARM.attributes ARM_ATTRIBUTES 00000000 0000fe 000034 00 0 0 1 [12] .symtab SYMTAB 00000000 000134 000130 10 13 18 4 [13] .strtab STRTAB 00000000 000264 00003b 00 0 0 1 [14] .shstrtab STRTAB 00000000 0002c8 000071 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), y (purecode), p (processor specific)
.ld файл линковщика
/* Compact ET_REL layout */ ENTRY(module_initialize) SECTIONS { .text : { *(.text .text.*) } .rodata : { *(.rodata .rodata.*) } .data : { *(.data .data.*) } .bss : { *(.bss .bss.* COMMON) } .init_array : { KEEP(*(.init_array)) KEEP(*(.init_array.*)) } .fini_array : { KEEP(*(.fini_array)) KEEP(*(.fini_array.*)) } }
Итого, должны соблюдаться следующий ряд условий
тип ELF - REL (Relocatable file)
никаких import/export символов
конструктор промаркирован
никаких __aeabi_* импортов от математики, так как нет маппинга на встроенные lib, loader .ELF в insmod просто режекнет такой .ELF - не загрузит и выдаст ошибку.
Сборка модуля приложения
Теперь имплементируем интерфейсы и структуры в модуле приложения,
для регистрации приложения.
static char notification_title[] = "Voilà! ?"; static char notification_src[] = "Calculator"; static char notification_body[] = "I'm in apps. Find me there."; static char APP_ICON[] = "/data/calc_logo_112.png"; static struct notification_msg_t done_notification = { .message_id = UINT64_C(0xBE57BEEF), .title = notification_title, .source = notification_src, .body = notification_body, .small_icon_path = APP_ICON, .large_icon_path = APP_ICON, .start_reminder = 1u, }; static miwear_app_t g_calc_app = { .proto_app = 0, .field_4 = 0, .name = APP_NAME, .icon = APP_ICON, .app_id = CALC_APP_ID, .flags = 0, .field_14 = 0, .field_18 = 0, .app_get_name = calc_app_get_name, .on_uninstall = 0, .field_24 = 0, .field_28 = 0, .field_2C = 0, .field_30 = 0, .field_34 = 0, .on_signal = 0, .field_3C = 0, }; static miwear_page_t g_calc_page = { .parent_descriptor = 0, .page_name = PAGE_NAME, .page_id = CALC_PAGE_ID, .app_id = CALC_APP_ID, .flags = 0, .scheduler_deadline = 0, .scheduler_priority = 0, .async_destroy_state = 0, .lifecycle_state = 0, .layer = 1, .page_kind = 0, .activity_context = 0, .root_object = 0, .on_signal = calc_page_on_signal, .runtime_default_56 = 0, .registry_prev = 0, .registry_next = 0, .runtime_parent = 0, .on_create = calc_page_on_create, .on_resume = 0, .on_newdata = 0, .on_start = 0, .on_pause = 0, .on_stop = 0, .on_destroy = calc_page_on_destroy, .on_back = 0, .on_keyevent = 0, .get_scroll_obj = 0, }; static void register_app(void) { if (app_lookup(CALC_APP_ID) == 0) { /* Register the calculator app with its main page. */ miwear_page_t *pages[] = { &g_calc_page }; int res = g_packagemanager_api->install(&g_calc_app, pages, 1); syslog(LOG_WARN, "%s pm_app_install(%s) rc=%d\n", LOG_TAG, g_calc_app.name, res); /* Add the app icon to the launcher grid. */ int rc = app_launcher_add(CALC_APP_ID); syslog(LOG_WARN, "%s app_launcher_add rc=%d\n", LOG_TAG, rc); if (res == 0) { char *curr_face = g_watchface_config && g_watchface_config->current_face ? g_watchface_config->current_face->id : ""; syslog(LOG_WARN, "curr face: %s\n", curr_face); watchface_manager_delete_watchface(curr_face); watchface_manager_reset_watchface(NULL); lvx_notification_insert_message(&done_notification); } } else { syslog(LOG_WARN, "%s add already exists", LOG_TAG); } } static void timer_callback(lv_timer_t *timer) { (void)timer; lv_timer_delete(post_timer_id); post_timer_id = 0; register_app(); syslog(LOG_WARN, "%s timer fired", LOG_TAG); } static void call_deferred(void) { post_timer_id = lv_timer_create(timer_callback, 500, 0); syslog(LOG_WARN, "%s lv_timer_create -> %p\n", LOG_TAG, post_timer_id); } __attribute__((constructor)) int module_initialize(void) { syslog(LOG_WARN, "%s loaded\n", LOG_TAG); // There is a problem to call UI functions in shell thread. // So there is a solution, to run timer and callback will fired // in lvgl loop thread, otherwise it fuckup the shell thread // and system reboots. call_deferred(); return 0; }
Я думаю, что тут все понятно и без лишних слов,
я определяю глобальные обьекты g_calc_app и g_calc_page,
регистрирую приложение в системе через вызов
g_packagemanager_api->install(&g_calc_app, pages, 1);
системый вызов app_launcher_add добавляет приложение в грид лаунчера,
ну и дополительно, я добавил атоматическое удаление циферблата,
для этого дергаются два вызова. watchface_manager_delete_watchface, перед этим я разобрал структуру таблицы хранилища циферблатов системы, чтобы вытащить id текущего циферблата, ну и вызов watchface_manager_reset_watchface(NULL) нужен, чтобы система обновила UI после удаления, иначе циферблат удален, а на дисплее все равно отображается.
плюс, для улучшения визуалиции, и пробы функционала добавил вызов
системного уведомления через lvx_notification_insert_message
Непосредственно калькулятор
Создание страницы приложения калькулятора, скучная и не сильно интересная часть, поэтому большую часть кода я опушу, полное приложение, вы можете посмотреть на моем github.
Ссылка будет ниже.
Больше всего внимания заслуживает lvgl часть, математику я дал на реализацию LLM, единственно, что важно там, я указал компилятору что процессор умеет во float-point operations
через директивы -mfloat-abi=hard -mfpu=fpv5-sp-d16 и убедился что компилятор не нагенерил мне aeabi вызовов.
Итак
lv_obj_t *calc_page_create(lv_obj_t *parent) { /* Full-screen container. */ lv_obj_t *root = lv_obj_create(parent, 0); lv_obj_set_size(root, SCREEN_W, SCREEN_H); /* Background. */ lvx_obj_set_style_bg_color(root, 0xFF101018u); lv_obj_set_style_bg_opa(root, 255u, 0); /* Default lv_obj padding shifts every TOP_LEFT aligned child; clear * it so the calculator grid is positioned against true (0,0). */ lv_obj_set_style_pad_left (root, 0, 0); lv_obj_set_style_pad_right (root, 0, 0); lv_obj_set_style_pad_top (root, 0, 0); lv_obj_set_style_pad_bottom(root, 0, 0); /* Display label at the top. Spans full screen width with a uniform * left+right gap so it doesn't kiss either edge. */ lv_obj_t *disp = lv_label_create(root); calc_disp_label = disp; lv_label_set_text(disp, calc_display); lv_obj_set_size(disp, SCREEN_W - 16, CALC_DISP_H); lv_obj_align(disp, LV_ALIGN_TOP_RIGHT, -8, 12); lv_style_t *lb_style = (lv_style_t *)LB_TEXT_STYLE_1; lvx_obj_set_style_text(disp, lb_style, 255, 0); lv_obj_set_style_text_align(disp, LV_ALIGN_TOP_RIGHT, 0); /* Button grid -- 4 cols, 5 rows. The "0" key spans two columns. */ static const char *const labels[5][4] = { { "C", "<=", "%", "/" }, { "7", "8", "9", "*" }, { "4", "5", "6", "-" }, { "1", "2", "3", "+" }, { "0", "", ".", "=" }, }; lv_coord_t total_w = 4 * CALC_BTN_W + 3 * CALC_BTN_GAP; lv_coord_t start_x = (SCREEN_W - total_w) / 2; lv_coord_t start_y = CALC_DISP_H + 24; for (int r = 0; r < 5; ++r) { for (int c = 0; c < 4; ++c) { const char *lbl = labels[r][c]; if (lbl[0] == '\0') continue; lv_coord_t w = CALC_BTN_W; if (r == 4 && c == 0) w = 2 * CALC_BTN_W + CALC_BTN_GAP; lv_coord_t x = start_x + c * (CALC_BTN_W + CALC_BTN_GAP); if (r == 4 && c == 0) { /* Wide "0" spans columns 0-1; anchor it flush-left. */ x = start_x; } lv_obj_t *btn = lv_btn2_create(root); lv_obj_set_size(btn, w, CALC_BTN_H); lv_obj_align(btn, LV_ALIGN_TOP_LEFT, x, start_y + r * (CALC_BTN_H + CALC_BTN_GAP)); lv_btn_set_text(btn, lbl); /* Function row (top) a bit lighter; other keys dark-grey; * orange "=". */ if (lbl[0] == '=') lvx_obj_set_style_bg_color(btn, 0xFF9500u); else if (r == 0) lvx_obj_set_style_bg_color(btn, 0x5C5C5Cu); else lvx_obj_set_style_bg_color(btn, 0x333333u); lv_obj_add_event_cb(btn, calc_on_press, LV_EVENT_CLICKED, (void *)lbl); } } return root; }
Как видно, тут пошел стандартный lvgl, в данной модели, это v9.
Кстати, очень рад, что китайцы использовали эту популярнейшую либу, а не свой китайский PersimUI, который я уже встречал в ряде моделей, можно использовать SquareLine Studio / LVGL Pro, к примеру, для визуального построения UI и генерации кода.
Platform mapping
Собственно, самая сложная часть тут - это вытащить API из платформы, я использую IDA Pro, и гоняю LLM через MCP, предварительно разметив сегменты прошивки,
и добавив опорные точки для упрощения анализа, к сожалению, или к счастью, (пока не понятно ?) LLM справляется посредственно, приходится перепроверять все и добивать кучу того, что не может найти.
На текущий момент я реализовал приложение под 2 платформы, последние на текущую дату:
MB10 Pro CN: 3.101.043 | 3.121.043*
MB10 Pro GL: 3.201.016 | 3.221.016* * - это мои моды сток прошивки.
Упаковка в циферблат
Самый быстрый и простой вариант, через оригинальный редактор Easyface, и мой компилятор, ссылки приложены ниже, в исходниках подготовлены проекты циферблатов и выполняется деплой после компиляции, а в моем MiFitness mod есть админка библиотеки циферблатов, где можно загрузить в development секцию циферблатов и установить.
Build environment
gcc version 10.2.1 20201103
VScode build tasks включены в исходный код, так что можете легко собрать мои проекты и написать свое
Результат реализации
Итак, что мы имеем в итоге:
insmod акцепит модуль и загружает приложение в RAM, успешно ремаппит адреса.
приложение успешно регистируется и запускается
работает быстро, тут ни LUA ни AiotJS рядом не стояли
ну бонусом оно умеет все системные штуки и имеет доступ ко всему что есть в часах, это очень круто.
К слову - бинарник, что я в итоге получил - mb10p_app_3.101.043.elf - 6.424 bytes.
Ограничения реализации.
Поскольку данное приложение инстанцируется через запуск ручной команды, оно естественно будет потеряно при перезагрузке устройства, но его можно быстро переустановить, ну и часы долгоиграющие - поэтому не такая большая проблема.
A в будущем я планирую добавить user script инициализации при загрузке системы в мод прошивки, которые я выпускаю, соответсвенно можно будет добавлять свои приложения туда и система будет поднимать их на старте.
Что можно еще
Да, блин, у меня разбегаются глаза - что можно еще
можно добавить ответы на сообщения telegram/whatsapp
можно сделать кастомные шедулер и notification менеджер под любую задачу - метроном, томато таймер, water drink
можно делать вызовы в сеть и запускать на часах что угодно, ну к примеру, я уже реализовал через Lua циферблат и curl - Алису, управляю колонками и умным домом через них.
сделать кастомный центр уведомлений
можно заменить любое приложение своим
можно расширить API Lua модулей и AiotJS это кроме банального написания нативных приложений и игр.
Canopus
Исследование и работа над данным решением меня вдохновил проект китайского разработчика SearchStars, он же автор приложения Astrabox, написанное на Tauri, который первым разобрался с insmod, и сделал свой модуль Canopus.
Его модуль истанцирует девайс /dev/canopus, а потом он через Lua последством fd общается через read/write, элегантное и простое решение, поскольку интерграция Lua модулей не тривиальная вещь, а данный механизм доступен из коробки, так же модуль добавляет динамически страницы в приложение Canopus, которое точно так же регистрируется в система.
Поверх этого модуля он написал крутой модуль - в котором реализовал A2DP+AVDTP audio stack, который позволяет pair-ить колонки и наушники и играть звук в них, сейчас реализованы MB9/MB10 Pro, MB11.
Часть кода написана на Rust - так что кому интересно - внизу оставлю ссылки.
Вот так это выглядит на девайсе

NuttX OS binary loader - https://nuttx.apache.org/docs/latest/components/binfmt.html
NuttX OS insmod - https://nuttx.apache.org/docs/latest/applications/nsh/commands.html#insmod-install-an-os-module
NuttX OS Shell - https://nuttx.apache.org/docs/latest/applications/nsh/index.htmlCalculator App src https://github.com/m0tral/MiWatchNativeApps
Easyface - https://github.com/m0tral/EasyfaceCanopus - https://github.com/AstralSightStudios/Canopus
Canopus-Module-BluetoothAudio https://github.com/Searchstars/Canopus-Module-BluetoothAudioTG news - https://t.me/mi_watch_news TG чат - https://t.me/mi_watch_int