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

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


  1. AeYronu
    06.09.2026 18:19

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

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

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