how Mi Smart Band 7 will help you not to forget important things

Use the navigation if you don't want to read the whole thing:

Statement of the problem
Software and hardware platform
Developing a beautiful application
Functional application development
Dial modification
Conclusion

Statement of the problem


The task is simple: a fitness bracelet should allow you to mark and display the completion of a daily routine action that the user does “automatically.” Let's go over the workflow.

  1. When performing a daily activity, a person enters information about it into the fitness bracelet. It takes some getting used to.
  2. When the thought “I forgot to do…” arises, you can look at the bracelet and determine exactly whether you forgot or not.
  3. At night, the fitness bracelet automatically resets the entered actions so that after waking up you can mark them “from scratch.”

This gives rise to wishes and clarifications, from which a “counter” arises, incremented by the wearer of the bracelet.

  • Unfortunately or happinessthe bracelet cannot reliably detect human micro-actions, so you need to enter information manually. However, I would like to reduce the number of additional actions to a minimum.
  • The solution should be relatively universal. Today, for example, it is important to note the morning warm-up, tomorrow – the number of teeth brushed, and the day after tomorrow – to count how many times colleagues called for tea.

Let's see what we have and how to work with it.

Software and hardware platform


I have at my disposal the Mi Smart Band 7 fitness bracelet, which is called Mi Band from old memory. It runs on the ZeppOS operating system, like other modern Amazfit brand products. This OS allows you to develop watch faces and individual applications for bracelets and watches. The eighth and current ninth generations of Mi Smart Band work on Xiaomi’s own solution, which does not yet have such variety.

Officially, the seventh generation does not support ZeppOS, so this is a rather sad situation for enthusiasts. The Xiaomi band works with Mi Fitness and Zepp Life, which do not use all the available features of ZeppOS, including the ability to install apps, and Zepp and the development tools are reluctant to work with the Smart Band 7.

Two years ago Vadim170 in his review described the “technical magic” that deceives the Zepp application and passes off the Mi Smart Band 7 as Amazfit Band 7. These bracelets have different screen resolutions, so you will no longer be able to set watch faces through the official application. However, you can remove some device restrictions, including using developer mode – Bridge.

Careful Study forum 4pda led me to the following conclusions.

  • You will have to write it in JavaScript.
  • Resource files (images) are encoded in specific formats.
  • Smart people have long made development tools for Mi Smart Band 7.
  • Official ZeppOS documentation In some places it is incorrect and requires clarification.
  • Some functions work in a non-obvious and unusual way. For example, rotate image widget rotates not the widget itself, but the image inside, cropping its edges. You need to independently calculate the size of the widget that will accommodate the rotated image.
  • Using ZeppOS's “limited” features is possible, but there will be surprises.

To get acquainted with the tools, I set an abstract goal: to develop an application from scratch that will generate random numbers from 1 to 20. In other words, simulate a 1d20 die roll. Sounds easy, let's get started.

Developing a beautiful application

Zepp OS Simulator with Mi Smart Band 7.

First, I looked towards the official tools, including a device simulator on ZeppOS. The application is called simulator and launches QEMU with the device firmware. By default there is no Mi Smart Band 7, but after upgrading simulator files it can be downloaded.

I ran into problems almost immediately when using the official tools.

  • The simulator application crashes on Ubuntu 24.04, but runs on Windows.
  • To download the device simulator, you need to log in with your Zepp profile. The web version has login buttons through other services, for example, Xiaomi, and inside the simulator you can only log in using your login and password. I had to create a separate account.
  • The virtual machine itself creates some inconveniences: when focusing on the bracelet, sometimes pressing on the screen gets stuck. The solution is to remove focus using the Ctrl+Alt+G key combination.
  • On 4pda note that the behavior of JS in the emulator and on a real device is different.

ZeppPlayer.

Luckily, there is an alternative developed by 4pda user MelianMikoZeppPlayer. The simulator exposes APIs that are available to watch faces and apps, but runs them entirely in the browser's JS engine. Compared to the official simulator, ZeppPlayer has a number of advantages. Let's look at the key ones.

  • There is no need to rebuild or deploy the application: ZeppPlayer works with source files directly and automatically applies updates.
  • You may not want to transcode images to a ZeppOS compatible format.
  • It’s convenient to create and test watch faces: you can set the time, date and sensor readings yourself. There is also a separate button for automatically creating an animated preview.
  • It is possible to look into the structure of the watch face or application.

However, despite the convenience, you need to remember that the only hardware of the bracelet is the rounded shape of the screen. On “real hardware” there will be a different JS interpreter and limited computing resources.

In your Telegram channel I wrote a note about JavaScript Mi Smart Band 7. Subscribe, there you can see notes on the topics of the articles I’m working on, and small educational posts.

In addition to ZeppPlayer, MelianMiko created a utility

zmake

which converts resources into a format suitable for ZeppOS and assembles a “binary” file for installation on the bracelet. The utility can also create app and watch face templates.

Let's create an application in the directory for ZeppPlayer.

E:\sb7\zmake>mkdir E:\sb7\ZeppPlayer\projects\habr
E:\sb7\zmake>zmake.exe E:\sb7\ZeppPlayer\projects\habr
Use config files:
  E:\sb7\zmake\zmake.json
We think that you want to create new project in this empty dir
Select new project type:
w - Watchface
a - Application
['w', 'a'] > a
E:\sb7\zmake>

We select the project in ZeppPlayer and… We get an error due to the re-definition of the __$$module$$__ identifier.

Error in project template.

However, there is a solution. We place all the code from the habr/page/index.js file into an anonymous lambda function:

(() => {
  // Здесь код, который был в файле.
})();

This anonymous lambda function call is found in many projects and is present in all js files except the entry point – app.js. Briefly, the project structure looks like this:

habr/
  assets/ - тут ресурсы (изображения), допускается вложенность
  page/ - тут js-файлы, реализующие страницы приложения или циферблата
  app.js - точка входа в приложение
  app.json - «манифест», описание приложения или циферблата

The user explained the project structure in detail and clearly

nusuthin his post on 4pda

. The app differs from the watch face in the parameter values ​​in app.json and the availability of widgets. Only app widgets are available for an app, and watch face and app widgets are available for watch faces.

My initial idea was to have a dice roll animation like in Baldur's Gate 3. Some people are angry about this mechanic, but I found it interesting. The throw animation can be divided into two parts: the prepared animation with the dice blurring and the manifestation of the result. For a prototype, one is enough, which shows a fixed result. We will solve problems iteratively as they arise.

There is a widget in the documentation IMG_ANIMwhich plays an animation from individual frames. Great, let's add it to page/index.js.

(() => {
        let __$$app$$__ = __$$hmAppManager$$__.currentApp;
        let __$$module$$__ = __$$app$$__.current;
        __$$module$$__.module = DeviceRuntimeCore.Page({
          onInit() {
                hmUI.createWidget(hmUI.widget.IMG_ANIM, {
                        x: 0, 
                        y: 0, 
                        anim_path: "dice",
                        anim_prefix: "roll",
                        anim_ext: "png",
                        anim_fps: 30,
                        repeat_count: 1,
                        anim_size: 91,
                        anim_status: hmUI.anim_status.START,
                });
          },
          onDestroy() {
                // On destroy, remove if not required
          }
        });
})();

The widget creates animation from files whose names are formed like this: assets/{anim_path}/{anim_prefix}_{i}. {anim_ext}, and the number i varies from 0 to anim_size-1. I generously set the animations to 30 frames per second, hoping that the bracelet would cope.

I had to open Blender.

Unfortunately, I couldn't find a dice roll animation that I could reuse in the project. Took it 3D model of a 20-hedronwhich is distributed under the CC BY-SA 4.0 license, based on a couple of video tutorials, I made an animation in Blender and rendered 90 frames of animation + 91 frames with the result.

Animation render.

This is where my first suspicions crept in. ZeppPlayer allows you to set any value for the fps parameter. You can even set it to 60 or 120 and the browser will draw the animation without any problems, while the bracelet’s screen, according to subjective sensations, is capable of producing about 30 frames per second. Let's test it in practice and assemble an application for deployment on a bracelet.

E:\sb7\zmake>zmake.exe e:\sb7\ZeppPlayer\projects\dice
Use config files:
  E:\sb7\zmake\zmake.json
We think that you want... build this project
Processing app.json:
  Done
Processing assets:
  91 saved in TGA-32 format
Copying common files:
  Done
Processing app.js:
Done
Processing "page" JS files:
  Copied 1 files
  Post-processed 1 files
Packaging:
Skip: \.gitignore
  Created BIN/ZIP files
Completed without error.
E:\sb7\zmake>

After the build, the build and dist directories appear in the project. The first contains a project ready for packaging, and the second contains a bin file and a zip archive with a bin file.

After the first build and page update, ZeppPlayer will not look to the project's source code, but to the project built in the build directory. Remove the build directory and refresh the page to continue debugging without a permanent build.

Installation of applications and third-party watch faces is carried out in the same way – through the application “

Mi Band 7 Watch Faces from Mi Band Watch Face Makers

  1. We transfer the bin file to the phone.
  2. We launch Zepp Life and wait for connection to the bracelet.
  3. Go to the third-party application, select the bin file and direct installation.

I was never able to find out whether the Mi Band is capable of reproducing such an animation. The bracelet displayed a warning about low storage space every 30 seconds, and the application itself launched only as a black screen.

Here I fell into another trap of the “unofficial” nature of ZeppOS: applications can be installed, but not deleted! There are two solutions: do a complete reset of the bracelet or install Toolbox by MelianMiko and use it to delete your application. I deleted a couple of watch faces and, catching the moment when the bracelet stopped complaining about memory, I installed Toolbox and got rid of my application.

Realistic animations are not about Mi Smart Band 7.

Functional application development

Conclusions have been drawn and lessons have been learned. Let the application be minimalistic, without long animations and other “tricks”. The first screen is a selection menu with three options.

The second screen is randomly generated numbers with appropriate design. Let's start with the first one.

(() => {
        let __$$app$$__ = __$$hmAppManager$$__.currentApp;
        let __$$module$$__ = __$$app$$__.current;
        __$$module$$__.module = DeviceRuntimeCore.Page({
          onInit() {
           // Повторить три раза...     
                hmUI.createWidget(hmUI.widget.IMG, {
                        x: 29, 
                        y: 50, // ...с разными y-координатами, ...
                        src: "roll/d20-adv.png",
                }).addEventListener(hmUI.event.CLICK_DOWN, (info) => {
                        hmApp.gotoPage({
                                url: "page/d20",
                                param: "adv" // ... и аргументами
                        });
                })
          },
          onDestroy() {}
        });
})();

The basic mechanics look something like this: we create a widget and, if necessary, add an event response for it. To open a new page, you need to call the gotoPage function from the hmApp global object. Some global objects are documented

in Watchface API

but with hmApp you will have to peek into other people’s solutions. Fortunately, everything is simple here: url is the path to the js file without extension inside the page directory, and param is the argument that is passed to the onInit function.

Let's move on to the second screen: create the file page/d20.js.

(() => {
        let __$$app$$__ = __$$hmAppManager$$__.currentApp;
        let __$$module$$__ = __$$app$$__.current;
        __$$module$$__.module = DeviceRuntimeCore.Page({
          onInit(arg) { // функция принимает аргумент от прошлой страницы
                if(arg == "adv" || arg === "dis") {
                        const result = [
                                Math.ceil((Math.random() * 20)),
                                Math.ceil((Math.random() * 20))
                        ];
                        // Рисуем два виджета
                } else {
                        const result = Math.ceil((Math.random() * 20));
                        let color;
                        if(result === 1) {
                                color = 0xff0000; 
                        } else if (result === 20) {
                                color = 0x00ff00;
                        } else {
                                color = 0x000000; 
                        }
                 hmUI.createWidget(hmUI.widget.IMG, {
                                x: 49, 
                                y: 193, 
                                src: "roll/d20-blue.png",
                        })
                 // Каждый следующий виджет перекрывает предыдущий.
                 // Рисуем текст после картинки
                        hmUI.createWidget(hmUI.widget.TEXT, {
                          x: 49,  // Растягиваем текстовое поле
                          y: 193, // на всю картинку
                          w: 93,
                          h: 103,
                          color: color,
                          text_size: 36,
                   // просим виджет центрировать текст
                          align_h: hmUI.align.CENTER_H, // по горизонтали
                          align_v: hmUI.align.CENTER_V, // и вертикали
                          text: result
                        })
                }
          },
          onDestroy() {}
        });
})();

There is no need to think about returning to the main menu: swiping from left to right returns you to the previous screen. We assemble, launch and everything seems to work. Application source code available

on Github

and the binary for installation on the bracelet is in the releases. We've learned the basics, so let's get back to the original problem.

Dial modification

Favorite dial. Source.

Let me remind you that one of the wishes is a minimum number of steps to enter information into the bracelet, so developing an application is a long journey. You need to open the menu, find the application and do something in it. I would like to enter information on the main screen, preferably with one touch.

I've been using the dial for a long time Handy from the user itBAIT. I especially like the three circles on which you can display the values ​​of any of the eleven sensors, and clicking on an element opens the corresponding application. Here, as organically as possible, you can fit a clicker counter, which will increase its value by one when pressed.

I contacted the watch face's author for permission to make and publish the modification and received a positive response. Respect other people's work!

The watch face is distributed as a bin file, which is actually a zip archive. Change the extension and unpack it into the projects directory. Inside are already known assets and watchface instead of page. The watchface directory contains index.js, a file that describes all the watch face widgets and its settings.

In some cases, instead of a js file there will be bin files. This is interpreter bytecode; at the moment there is no way to decompile it into JavaScript.

As it turns out, the skills learned while developing the app were very useful: the watch face is all about initializing the various widgets in the correct order, with a few exceptions.

  • The watch face has settings (WATCHFACE_EDIT_GROUP) and the OS itself takes care of saving data.
  • Most widgets have a type field that allows you to specify the “source” of the data. In this case, the operating system itself updates the widgets. The exception is the progress bar for the sleep metric. The author defined a function that reads the sleep metric and sets the percentage for the progress bar. It is called by a timer once every four seconds.

The author cleaned up the code before publishing the watch face, thereby making my work easier. First, we define a new type of widget and draw three icons: for settings, color and white.

const widgets = [
    /* id 0-10 удалены для краткости */
    {
        id: 11,
        name: "sleep",
        title: "Sleep time",
        "ru-RU": "\u0412\u0440\u0435\u043C\u044F \u0441\u043D\u0430",
        color: 6291711,
        background: 1900621,
        type: hmUI.data_type.SLEEP,
        url: "Sleep_HomeScreen"
    },<br>    // Новый виджет
    {
        id: 12,
        name: "counter",
        title: "Counter",
        "ru-RU": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a",
        color: 0x9999FF,
        background: 0x5A5A96
    },
    {
        id: 99,
        // empty widget
        title: "Empty",
        "ru-RU": "\u041F\u0443\u0441\u0442\u043E"
    }
];

Blank widget.

Now the widget can be selected in the settings, but it has no readings. Storing them is an issue that is not covered in classic watch faces, since the OS abstracts us from this. It will not work to use global variables: opening a page completely resets the one being closed.

The Watchface API documentation contains a description of the global object hmFSwhich provides functions for working with the file system and temporary storage. I don’t want to touch the first one, but temporary key-value storage is what’s needed.

// инициализация круговых виджетов
widgetsEditables.forEach((edit, index) => {
    /* Удалено для краткости */
    const bar = hmUI.createWidget(hmUI.widget.ARC_PROGRESS, /* params */)
    const text = hmUI.createWidget(hmUI.widget.TEXT_IMG, /* params */)
    
    if (name === "sleep") {
        /* Особый обработчик для метрики сна */
    } else if(name === "counter") {
        /* Добавляем свой обработчик для счетчика */
        const callback = () => {
            /* Если день сменился, сбрасываем счетчик */
            const time = hmSensor.createSensor(hmSensor.id.TIME)
            if (hmFS.SysProGetInt("watchface_handy_mod_counter_data_day") !== time.day) {
                hmFS.SysProSetInt("watchface_handy_mod_counter_data_payload", 0)
            }

            /* Выводим значение из хранилища на виджет-цифру и на шкалу прогресса */
            const payload = hmFS.SysProGetInt("watchface_handy_mod_counter_data_payload");
            text.setProperty(hmUI.prop.TEXT, String(payload))
            bar.setProperty(hmUI.prop.MORE, {
                ...barProps,
                level: payload * 10 < 100 ? payload * 10 : 100
            });
        };
        callback();
        timer.createTimer(0, 4e3, callback);
    }
}

Similarly, we register clicks on widgets

if (options.tapzones) {
    if(name === "counter") {
        onClick(
            () => {
                /* Обнуляем счетчик при смене даты, если этого не произошло ранее */
                const time = hmSensor.createSensor(hmSensor.id.TIME)
                let counter = 0;
                if (hmFS.SysProGetInt("watchface_handy_mod_counter_data_day") === time.day) {
                    counter = hmFS.SysProGetInt("watchface_handy_mod_counter_data_payload")
                }
                /* Увеличиваем на единицу*/
                counter += 1;
                /* Сохраняем в хранилище */
                hmFS.SysProSetInt("watchface_handy_mod_counter_data_day", time.day)
                hmFS.SysProSetInt("watchface_handy_mod_counter_data_payload", counter)
                /* Отображаем на виджетах, не дожидаясь срабатывания таймера */
                text.setProperty(hmUI.prop.TEXT, String(counter))
                bar.setProperty(hmUI.prop.MORE, {
                    ...barProps,
                    level: counter * 10 < 100 ? counter * 10 : 100
                });
            },
            bar,
            icon,
            text
        );
    }
}

The storage keys are intentionally long since it is shared across all apps and watch faces. Side effect – the documentation says that the temporary storage is cleared upon reboot, but this is not the case on the bracelet. At least when rebooting through the settings, the bracelet retains the clicked value.

Demonstration. When the time changes, the day changes and the counter resets automatically.

When modifying the watch face, I once again encountered the difference between ZeppPlayer and ZeppOS:

let counter = 5;

/* Назначение типом number выводит 0 на браслете */
text.setProperty(hmUI.prop.TEXT, counter);

/* Назначение с явной конвертацией к строке выводит 5, как и ожидалось */
text.setProperty(hmUI.prop.TEXT, String(counter));

/* Для ZeppPlayer оба варианта выводят 5 */

Conclusion

The result is a fairly universal counter. However, even though it is in a prominent place, it is still lost in the many indicators of a fitness bracelet. Also, the need to mark the morning routine on the bracelet leads to the fixation of the action in memory, so that the original idea is poorly implemented. But maybe in a month something will change…

You can download the modified dial follow the link. Perhaps it will appear on 4pda later.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *