- PVSM.RU - https://www.pvsm.ru -
Итак, сейчас у меня на руках последняя модель из линейки 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 [1]
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”) для запуска/истанцирования.
Разработчики Longcheer разработали и написали довольно неплохой структурированный и продуманный код системы, с выделением подсистем часов в отдельные независимые и расширяемые модули, одно из них, и интересных нам - подсистема приложений.
Ключевые моменты этой подсистемы:
весь интерфейс часов разделен логически на отдельные приложения
для управления приложениями выделена отдельная сущность - менеджер приложений.
для запуска приложение должно быть зарегистрировано, и если необходимо - добавлено в launcher.
Вот так выглядит 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=%dn",
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=%dn", 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: %sn", 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 -> %pn", LOG_TAG, post_timer_id);
}
__attribute__((constructor)) int module_initialize(void)
{
syslog(LOG_WARN, "%s loadedn", 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] == '') 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 и генерации кода.
Собственно, самая сложная часть тут - это вытащить 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 секцию циферблатов и установить.
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 это кроме банального написания нативных приложений и игр.
Исследование и работа над данным решением меня вдохновил проект китайского разработчика SearchStars, он же автор приложения Astrabox, написанное на Tauri, который первым разобрался с insmod, и сделал свой модуль Canopus.
Его модуль истанцирует девайс /dev/canopus, а потом он через Lua последством fd общается через read/write, элегантное и простое решение, поскольку интерграция Lua модулей не тривиальная вещь, а данный механизм доступен из коробки, так же модуль добавляет динамически страницы в приложение Canopus, которое точно так же регистрируется в система.
Поверх этого модуля он написал крутой модуль - в котором реализовал A2DP+AVDTP audio stack, который позволяет pair-ить колонки и наушники и играть звук в них, сейчас реализованы MB9/MB10 Pro, MB11.
Вот видео как оно работает [2]
Часть кода написана на Rust - так что кому интересно - внизу оставлю ссылки.
NuttX OS binary loader - https://nuttx.apache.org/docs/latest/components/binfmt.html [1]
NuttX OS insmod - https://nuttx.apache.org/docs/latest/applications/nsh/commands.html#insmod-install-an-os-module [3]
NuttX OS Shell - https://nuttx.apache.org/docs/latest/applications/nsh/index.html [4]Calculator App src https://github.com/m0tral/MiWatchNativeApps [5]
Easyface - https://github.com/m0tral/Easyface [6]Canopus - https://github.com/AstralSightStudios/Canopus [7]
Canopus-Module-BluetoothAudio https://github.com/Searchstars/Canopus-Module-BluetoothAudio [8]TG news - https://t.me/mi_watch_news [9] TG чат - https://t.me/mi_watch_int [10]
Автор: m0tral
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-programmirovanie/457575
Ссылки в тексте:
[1] документации NuttX: https://nuttx.apache.org/docs/latest/components/binfmt.html
[2] видео как оно работает: https://www.youtube.com/shorts/qLeVs3hcitM
[3] https://nuttx.apache.org/docs/latest/applications/nsh/commands.html#insmod-install-an-os-module: https://nuttx.apache.org/docs/latest/applications/nsh/commands.html#insmod-install-an-os-module
[4] https://nuttx.apache.org/docs/latest/applications/nsh/index.html: https://nuttx.apache.org/docs/latest/applications/nsh/index.html
[5] https://github.com/m0tral/MiWatchNativeApps: https://github.com/m0tral/MiWatchNativeApps
[6] https://github.com/m0tral/Easyface: https://github.com/m0tral/Easyface
[7] https://github.com/AstralSightStudios/Canopus: https://github.com/AstralSightStudios/Canopus
[8] https://github.com/Searchstars/Canopus-Module-BluetoothAudio: https://github.com/Searchstars/Canopus-Module-BluetoothAudio
[9] https://t.me/mi_watch_news: https://t.me/mi_watch_news
[10] https://t.me/mi_watch_int: https://t.me/mi_watch_int
[11] Источник: https://habr.com/ru/articles/1079066/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1079066
Нажмите здесь для печати.