intro
intro

Итак, сейчас у меня на руках последняя модель из линейки 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 - он более простой для сборки приложения. Итак, на данном этапе, у нас есть два важных момента, как будем запускать приложение:

  1. Используем LUA циферблат для деплоя(загрузки) .elf приложения в часы

  2. Вызовем команду 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 - так что кому интересно - внизу оставлю ссылки.

Вот так это выглядит на девайсе

final result
final result

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.html

Calculator App src https://github.com/m0tral/MiWatchNativeApps
Easyface - https://github.com/m0tral/Easyface

Canopus - https://github.com/AstralSightStudios/Canopus
Canopus-Module-BluetoothAudio https://github.com/Searchstars/Canopus-Module-BluetoothAudio

TG news - https://t.me/mi_watch_news TG чат - https://t.me/mi_watch_int

Комментарии (10)


  1. AeYronu
    06.09.2026 18:19

    Отлично! Это именно то чего не хватает. И не только этой модели. А вообще всей продукции от Xiaomi/Redmi.

    Этот решение уже есть на 4PDA?

    Можно ли как то портировать данное решение на Redmi Watch 5 Active?


    1. m0tral Автор
      06.09.2026 18:19

      Нельзя, Active линейка говно, на дешевом железе и FreeRTOS обычно, чтобы делать такое нужно чтобы было

      1. NuttX OS - linux подобная система

      2. Lua engine

      3. Shell содержал рабочий insmod загрузчик

      Например, первые часы у Сяоми на NuttX OS - Watch S1 Pro,
      там есть 2 из 3, но нет Lua engine.
      У Redmi Watch 5 - Lua engine просто вырезан, тоже нельзя таким способом как я показал, но можно через обновление прошивки.

      На Redmi Watch 5 Active - проц не Bestechnic, a там платформа
      SiFli SF32LB551 - https://wiki.sifli.com/en/silicon/index.html
      SRAM 1Mb, internal flash - 0, external SPI nand 2x8Mb,
      на двух внешних nand чипах, на которых хранилище циферблатов 2.6мб
      на них даже OTA пакет не полноценный, ресурсы на часах не обновляются, только приложение, потому что тупо нет места куда его сохранить при обновлении, 2 внешних nand флеш по 8мб - видимо у кого то осталось много этого говна в закормах, я не могу иначе это обьяснить

      Модели на которых такое возможно:

      mb9, mb10, mb11,
      mb8/mb9/mb10 pro
      RW4, RW5(c оговоркой), RW6, RW7 (выйдут в ~декабре)
      Watch S3, S4, S5


  1. NutsUnderline
    06.09.2026 18:19

    xiaomi очень стараеться не быть amazfit (huami). Вероятно они не делают api для простых часов чтобы не вредить продажам их более дорогих часов на wearos


    1. kenomimi
      06.09.2026 18:19

      Скорее всего, это никому, кроме 3.5 гиков, не нужно. Я вот не представляю, какое я бы сделал приложение для своих часов - там есть всё, что нужно на смарт-часах.


      1. NutsUnderline
        06.09.2026 18:19

        такая же нога, в общем :) на 4pda люди высказывают очень разные желания по функционалу часов. например - "плыву на лодке и хочу видеть скорость". проще дать людям возможность реализовать то на что фантазии разработчиков не хватило. и еще более удивительно оказывается что уже реализовано. например я очень сильно удивился увидев приложение для homeassistant для часов amazfit. по числу скачиваний в соотв. маркете в принципе можно судить что гиков оказалось больше чем 3.5


    1. m0tral Автор
      06.09.2026 18:19

      вы о чем, документация на API и IDE для движка AiotJS существуют уже несколько лет
      https://iot.mi.com/vela/quickapp/en/tools/
      а сам движок впервые был еще на часах Xiaomi Watch S1/Color 2 - тогда их производила компания 70mai, на хорошо документированном микроконтроллере STM32U5

      По Lua же нет хорошей документации, хотя там почти весь API стоковые либы Lua 5.4

      Сяоми не сильно заботит глобальный рынок, их внутренний явно потребляет отлично,
      и я представляю почему, иногда проще забить x, чем проходить регуляцию стран задротов типа Германии, а России и подавно..


    1. m0tral Автор
      06.09.2026 18:19

      а продажи флагманов Mi Band компании и рядом не стояли с wearOS линейкой,
      я купив в 2014 году LG Watch R - до сих пор не вижу подвижек в сторону увеличения автономности моделей, и я не куплю wearOs/Apple только поэтому, компаниям насрать на потребиля и вы ничего не можете с этим сделать, а в моделях Сяоми я получаю и крутую автономность и систему в которой я могу все что мне нужно.

      а маржу они сейчас берут за счет продаж керамики и золотых(цыганских) версий своих дешевых моделей.


      1. NutsUnderline
        06.09.2026 18:19

        кстати в xioaomi watch 5 говорят несколько улучшили


  1. v_0ver
    06.09.2026 18:19

    Спасибо за статью, у меня тоже Mi Band 10 Pro и я давно хотел сделать пару игр на акселерометре. Но никак не мог подступиться.


    1. m0tral Автор
      06.09.2026 18:19

      для этого достаточно обычного Lua - примеры с акселерометром у меня давно на github
      https://github.com/m0tral/MiWatchLuaWatchfaces

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