Всем привет, меня зовут Алексей Ляховский, я на протяжение последних 10 лет занимаюсь изучением, разработкой и развитием экосистемы часов Xiaomi для глобального сообщества.
Сегодня у меня в работе самый популярный продукт линейки часов Xiaomi - Mi Band 9.
Предыдущее поколение часов этой серии, но это не так важно, поскольку текущий Mi Band 10 не сильно отличается от нашего обозреваемого пациента.
В данных моделях есть специальный режим мониторинга температуры, который, в случае перегрева выключает часы, это не очень удобно, так как чтобы его включить нужен провод питания, сегодня мы посмотрим как это работает и я покажу как изменить поведение системы даже не меняя прошивку.

Что же под капотом
Итак, даже если вы никогда не носили данный браслет, то скорее всего видели что представляет собой браслет и как выглядит система. Но мало кто знает что за система внутри и как устроен браслет. А это очень интересно и увлекательно:
Во-первых, с 9ки Xiaomi стали использовать китайские процессоры BES2700, BES2800 компании Bestechnic, они достаточно производительные, имеют прилично flash памяти, в нашем случае 8Mb, PSRAM 16Mb, графический ускоритель, интегрированный BT контроллер и куча разной периферии.

Дополнительно у mb9 имеется 256Mb nand flash памяти для системы и хранения данных.
Так называемая HyperOS тут - это операционная система - Apache NuttX, очень приличная realtime OS уровня простенького Lunix, c shell на борту и пакетом поставляемых команд (вот тут можно ознакомиться подробнее - https://nuttx.apache.org/docs/latest/applications/nsh/index.html)
Например bootloader на базе NuttX для mb9 занимает всего 128Kb.
Также, система HyperOS, которую Xiaomi еще называет VelaOS (она так и называлась, до глобального переименовывания всех продуктов под HyperOS) включает в себя 2 движка для приложений: Lua и AiotJS (в девичестве JerryScript). Графический интерфейс системы реализован на Lvgl - очень популярная графическая библиотека в embedded (подробнее - https://lvgl.io/)
Движок AiotJS используется для приложений, а движок Lua для циферблатов.
Lua в данных часах версии 5.4 с библиотекой lvgl, собственно чтобы можно было рисовать свои интерфейсы в циферблатах.
Анализ прошивки браслета
Чтобы понять что и как можно сделать с данной проблемой перегрева,
я провел анализ прошивки с помощью всеми любимой IDA Pro.
В детали вдаваться не буду, это отдельная тема для разговоров.
Вот что я обнаружил в итоге:
Функция system_callback_temp
int __fastcall system_callback_temp(float **_temp)
{
float board_temp; // s16
int result; // r0
int v3; // r4
int v4; // r4
__int64 v5; // [sp+10h] [bp+0h] BYREF
board_temp = **_temp;
unk_200A509F = dword_2009D6E0[31];
unk_200A52A0 = unk_200A529C;
time_diff = get_timestamp(&v5) - body_hand_wrist_time;
result = syslog(
3,
"[%s] %s: callback_temp called %d %d %d\n",
"system",
"callback_temp",
unk_200A52A0,
unk_200A529C,
*(_DWORD *)&is_watch_is_on_hand);
if ( *(_DWORD *)&is_watch_is_on_hand == 1 )
{
if ( cool_strategy_43_copy && board_temp < 41.0 )
{
cool_strategy_43_copy = 0;
cool_strategy_43 = 0;
result = syslog(4, "[%s] %s: Exit the 43-degree cooling strategy\n", "system", "callback_temp");
}
if ( cool_strategy_41_copy && board_temp < 39.0 )
{
cool_strategy_41_copy = 0;
cool_strategy_41 = 0;
result = syslog(4, "[%s] %s: Exit the 41-degree cooling strategy\n", "system", "callback_temp");
}
if ( temp_strategy_autobritghness_off && board_temp < 37.0 )
{
temp_strategy_autobritghness_off = 0;
cool_strategy_39 = 0;
if ( is_auto_brightness )
sub_2C442BA8(1, &cool_strategy_39);
result = syslog(4, "[%s] %s: Exit the 39-degree cooling strategy\n", "system", "callback_temp");
}
if ( board_temp >= 39.0 && time_diff > 179 )
{
if ( !cool_strategy_39 )
{
cool_strategy_39 = 1;
syslog(4, "[%s] %s: Start the 39-degree cooling strategy\n", "system", "callback_temp");
result = system_ui_and_strategy(39);
}
if ( board_temp >= 41.0 )
{
v3 = cool_strategy_41;
if ( !cool_strategy_41 )
{
cool_strategy_41 = 1;
syslog(4, "[%s] %s: Start the 41-degree cooling strategy\n", "system", "callback_temp");
cool_strategy_41_prev = v3;
cool_strategy_41_val = 41;
cool_strategy_41_started = 1;
result = temp_relative_alarm(v3, (int)&cool_strategy_41_val);
}
if ( board_temp >= 43.0 )
{
v4 = cool_strategy_43;
if ( !cool_strategy_43 )
{
cool_strategy_43 = 1;
syslog(4, "[%s] %s: Start the 43-degree cooling strategy\n", "system", "callback_temp");
cool_strategy_43_prev = v4;
cool_strategy_43_val = 43;
cool_strategy_43_started = 1;
return temp_relative_alarm(v4, (int)&cool_strategy_43_val);
}
}
}
}
}
else
{
if ( cool_strategy_45 && board_temp < 46.0 )
{
cool_strategy_45 = 0;
result = syslog(4, "[%s] %s: Exit the 48-degree cooling strategy\n", "system", "callback_temp");
}
if ( cool_strategy_48 && board_temp < 43.0 )
{
cool_strategy_48 = 0;
result = syslog(4, "[%s] %s: Exit the 45-degree cooling strategy\n", "system", "callback_temp");
}
if ( board_temp >= 45.0 )
{
if ( !cool_strategy_45 )
{
cool_strategy_45 = 1;
cool_strategy_45_val = 45;
temp_relative_alarm(600, (int)&cool_strategy_45_val);
result = syslog(4, "[%s] %s: Start the 45-degree cooling strategy\n", "system", "callback_temp");
}
if ( board_temp >= 48.0 && !cool_strategy_48 )
{
cool_strategy_48 = 1;
cool_strategy_48_val = 48;
temp_relative_alarm(60, (int)&cool_strategy_48_val);
return syslog(4, "[%s] %s: Start the 48-degree cooling strategy\n", "system", "callback_temp");
}
}
}
return result;
}
Функция system_ui_and_strategy
int __fastcall system_ui_and_strategy(int temp)
{
int result; // r0
int v2; // r4
__int64 v3; // r0
result = temp - 39;
switch ( result )
{
case 0:
syslog(3, "[%s] %s: 39 temp will set brightness\n", "system", "ui_and_strategy");
is_auto_brightness = is_auto_brightness_0;
syslog(
6,
"[%s] %s: is_auto_brightness:%d",
"system",
"brightness_temp_charge",
(unsigned __int8)is_auto_brightness_0);
v2 = *(_DWORD *)&brightness_level;
if ( is_auto_brightness )
{
v3 = syslog(
6,
"[%s] %s: brightness_current_now:%d",
"system",
"brightness_temp_charge",
*(_DWORD *)&brightness_level);
brightness_set_auto_adjustment(0, HIDWORD(v3));
if ( v2 <= 128 )
{
if ( (unsigned int)(v2 - 0x20) > 96 )
{
brightness_set_value(31);
syslog(6, "[%s] %s: thermal set brightness to 31\n", "system", "brightness_temp_charge");
}
else
{
brightness_set_value(v2);
v2 = *(_DWORD *)&brightness_level;
syslog(
6,
"[%s] %s: thermal set brightness to %d\n",
"system",
"brightness_temp_charge",
*(_DWORD *)&brightness_level);
}
}
else
{
brightness_set_value(128);
v2 = *(_DWORD *)&brightness_level;
syslog(6, "[%s] %s: thermal set brightness to 128\n", "system", "brightness_temp_charge");
}
result = syslog(
6,
"[%s] %s: auto_brightness:ON->OFF brightness_current_after :%d",
"system",
"brightness_temp_charge",
v2);
}
else
{
result = syslog(
6,
"[%s] %s: brightness_current_now :%d",
"system",
"brightness_temp_charge",
*(_DWORD *)&brightness_level);
if ( v2 > 128 )
{
apply_brightness(128);
result = syslog(
6,
"[%s] %s: brightness_current_now afer :%d",
"system",
"brightness_temp_charge",
*(_DWORD *)&brightness_level);
}
}
temp_strategy_autobritghness_off = 1;
return result;
case 2:
if ( cool_strategy_41_started == 2 )
{
syslog(3, "[%s] %s: 41 temp start reminder window shutdown\n", "system", "ui_and_strategy");
goto LABEL_16;
}
return result;
case 4:
syslog(3, "[%s] %s: 43 temp start reminder window shutdown\n", "system", "ui_and_strategy");
goto LABEL_16;
case 6:
syslog(3, "[%s] %s: 45 temp start reminder window shutdown\n", "system", "ui_and_strategy");
goto LABEL_16;
case 9:
syslog(3, "[%s] %s: 48 temp start reminder window shutdown\n", "system", "ui_and_strategy");
LABEL_16:
if ( paired_flag == 2 )
{
sub_2C1641D4(3u);
result = system("poweroff");
}
else
{
result = temp_rise_reminder(0);
}
break;
default:
return result;
}
return result;
}
Как видно из кода, у алгоритма мониторинга на пороге 39 градусов Цельсия выключает автоматический уровень яркости, а затем на порогах 41, 43, 45 выключает браслет, если браслет на руке, когда же он лежит отдельно - пороги отключения 45, 48.
Посмотрим внимательно в код, запуск уведомления системы и последующее выключение происходит в методе temp_rise_reminder(int)
Функция temp_rise_reminder
int __fastcall temp_rise_reminder(int prm)
{
int v1; // r0
int v2; // r4
int result; // r0
char _prm; // [sp+7h] [bp+7h] BYREF
int v5[40]; // [sp+8h] [bp+8h] BYREF
_prm = prm;
syslog(6, "[%s] %s: enter temp reminder \n", "temp_reminder", "temp_rise_reminder");
v1 = malloc_(1);
v2 = v1;
if ( !v1 )
return syslog(3, "[%s] %s: temp data malloc fail\n", "temp_reminder", "temp_rise_reminder");
memcpy_(v1, &_prm, 1);
result = memset(&v5[1], 0, 156);
v5[0] = 0x220008;
v5[7] = -1;
LOWORD(v5[3]) = 2049;
v5[10] = temp_reminder_create_cb;
v5[2] = v2;
v5[11] = temp_reminder_delete_cb;
BYTE1(v5[9]) = -94;
v5[14] = temp_reminder_cb;
if ( !temp_reminder_in_progress )
{
result = lvx_reminder_start(v5);
if ( !result )
{
result = syslog(3, "[%s] %s: High and low temperature startup failure\n", "temp_reminder", "temp_rise_reminder");
}
}
return result;
}
и в данном методе есть замечательный флаг temp_reminder_in_progress, предотвращающий повторный вызов уведомления. Дак вот, его замечательность в том, что это статическая константа в памяти, линкер прошивки выделил ей статическое место в SRAM.

И это прекрасно, фокус в том, что прописав в эту переменную значение отличное от нуля, метод никогда не запустит уведомление, поскольку будет думать, что уведомление уже запущено.
Заодно найдем переменную в SRAM где хранится значение температуры браслета.
Вот код отвечающий за это
Функция get_temperature_finally
int __fastcall get_temperature_finally(int a1, int a2, int a3)
{
int handle; // r0
int v4; // r3
int _handle; // r4
__int16 temp; // [sp+Eh] [bp+6h] BYREF
temp = 0;
handle = open("/dev/charge", 65, a3, 0);
_handle = handle;
if ( handle < 0 )
return syslog(3, "[%s] %s: Failed to open charger device\n", "system", "get_temperature_finally");
if ( fb_ioctl(handle, 0xE0A, (int)&temp, v4) < 0 )
return syslog(3, "[%s] %s: failed to get temperature!", "system", "get_temperature_finally");
*(float *)&board_temp_val = (float)temp;
syslog(3, "[%s] %s: temp_reminder now temp is %d", "system", "get_temperature_finally", temp);
write((int)board_temp, dword_200A52A8, (int)&unk_200A4E00, (int)&dword_200A52A8);
return file_close(_handle);
}

и переменная температуры
Создаем циферблат на LUA
Использую полученные данные на предыдущем этапе, создадим Lua циферблат.Для работы с циферблатами я использую Xiaomi приложение Easyface с моим компилятором для нового формата циферблатов, в том числе для mb9 и mb10.

В нем идет линк на запускаемый файл проекта, все просто.
Главный код проекта выглядит так
main.lua
local lvgl = require("lvgl")
local temp = require("temperature")
local dataman = require("dataman")
local fsRoot = SCRIPT_PATH
local DEBUG_ENABLE = false
local selfFlag = false
local printf = DEBUG_ENABLE and print or function(...)
end
local function imgPath(src)
return fsRoot .. src
end
local rootbase = lvgl.Object(nil, {
w = lvgl.HOR_RES(),
h = lvgl.VER_RES(),
bg_color = 0,
bg_opa = lvgl.OPA(100),
border_width = 0,
})
rootbase:clear_flag(lvgl.FLAG.SCROLLABLE)
rootbase:add_flag(lvgl.FLAG.EVENT_BUBBLE)
local root = lvgl.Object(rootbase, {
outline_width = 0,
border_width = 0,
pad_all = 0,
bg_opa = 0,
bg_color = 0,
align = lvgl.ALIGN.CENTER,
w = lvgl.HOR_RES(),
h = lvgl.VER_RES(),
flex = {
flex_direction = "row",
flex_wrap = "wrap",
justify_content = "center",
align_items = "center",
align_content = "center",
}
})
root:clear_flag(lvgl.FLAG.SCROLLABLE)
root:add_flag(lvgl.FLAG.EVENT_BUBBLE)
local font60 = lvgl.Font("MiSans-Regular", 60)
local font30 = lvgl.Font("MiSans-Regular", 30)
local font26 = lvgl.Font("MiSans-Regular", 26)
local function createText(wgt)
return lvgl.Label(wgt, {
text_font = font60,
text = "Temp",
align = lvgl.ALIGN.CENTER,
border_color = '#eee',
border_width = 0,
text_color = '#eee'
})
end
local time = lvgl.Label(root, {
text_font = font26,
text = "00:00",
text_color = '#eee',
pad_bottom = 5
})
local title = lvgl.Label(root, {
text_font = font30,
text = "Band Temp",
text_color = '#eee',
pad_bottom = 20
})
title:add_flag(lvgl.FLAG.EVENT_BUBBLE)
local txt = createText(root)
txt:add_flag(lvgl.FLAG.EVENT_BUBBLE)
local function setText(str)
txt:set { text = str }
end
local temp1 = lvgl.Label(root, {
text_font = font26,
text = "Temperature",
text_color = '#eee',
pad_top = 40
})
local temp2 = lvgl.Label(root, {
text_font = font26,
text = "shutdowns",
text_color = '#eee'
})
temp1:add_flag(lvgl.FLAG.EVENT_BUBBLE)
temp2:add_flag(lvgl.FLAG.EVENT_BUBBLE)
local installWd = lvgl.Checkbox(root, {
text_font = font26,
text = "turned off",
text_color = '#eee',
pad_top = 10
})
local tempEnabled = temp:isEnabled()
if tempEnabled then
installWd:add_state(lvgl.STATE.CHECKED)
installWd:set { text = "turned on"}
end
installWd:add_flag(lvgl.FLAG.CLICKABLE)
installWd:onevent(lvgl.EVENT.CLICKED, function(obj, code)
if tempEnabled then
temp:disable()
installWd:set { text = "turned off"}
tempEnabled = false
else
temp:enable()
installWd:set { text = "turned on"}
tempEnabled = true
end
end)
dataman.subscribe("timeMinuteLow", time, function(obj, value)
local t = os.time()
local time_str = os.date("%H:%M", t)
time:set { text = time_str }
end)
dataman.subscribe("timeSecond", txt, function(obj, value)
setText(string.format("%.1f", temp:getTempFloat()))
end)
Здесь используется собственный модуль temperature, вот именно он и представляет самый большой интерес, остальное - это стандартный код циферблата.
temperature.lua
local watchVersion = require("watchVersion")
local memory = require("memory")
local math = require("math")
local temp = {
g_temp_ptr = 0,
g_reminder_ptr = 0,
init_done = false
}
local bodyTempMapping = {
["miwear.watch.n66cn"] = {
["1.3.206"] = 0x200A15E8
},
["miwear.watch.n66nfc"] = {
["1.3.206"] = 0x200A15E8
},
["miwear.watch.n66tc"] = {
["1.3.206"] = 0x200A15E8
},
["miwear.watch.n66gl"] = {
["2.3.97"] = 0x200A55A0
},
["unknown"] = {
["version1"] = 0x20000000,
["version2"] = 0x20000000
}
}
local tempReminder = {
-- MiBand 9
["miwear.watch.n66cn"] = {
["1.3.206"] = 0x200A1174
},
["miwear.watch.n66nfc"] = {
["1.3.206"] = 0x200A1174
},
["miwear.watch.n66tc"] = {
["1.3.206"] = 0x200A1174
},
["miwear.watch.n66gl"] = {
["2.3.97"] = 0x200A5150
},
["unknown"] = {
["version1"] = 0x20000000,
["version2"] = 0x20000000
}
}
local function getTempValueByVersion(model, version)
local modelVersions = bodyTempMapping[model]
if modelVersions then
return modelVersions[version]
end
return nil
end
local function getReminderValueByVersion(model, version)
local modelVersions = tempReminder[model]
if modelVersions then
return modelVersions[version]
end
return nil
end
local function uint32_to_float(u)
local sign = ((u >> 31) & 0x01)
local exponent = ((u >> 23) & 0xFF)
local mantissa = u & 0x7FFFFF
if exponent == 255 then
if mantissa == 0 then
return sign == 1 and -math.huge or math.huge
else
return 0/0 -- NaN
end
end
local value
if exponent == 0 then
-- denormalized
value = (mantissa / 2^23) * 2^-126
else
-- normalized
value = (1 + mantissa / 2^23) * 2^(exponent - 127)
end
return sign == 1 and -value or value
end
local function init()
if temp.init_done then
return
end
local model = watchVersion.get_model()
local ver = watchVersion.get_version()
local addr = getTempValueByVersion(model, ver)
if addr ~= nil then
temp.g_temp_ptr = addr
end
addr = getReminderValueByVersion(model, ver)
if addr ~= nil then
temp.g_reminder_ptr = addr
end
temp.init_done = true
end
function temp:readIntByAddress(addr)
if addr == 0 then
return
end
local maddr, res = memory:readAddr(addr)
if res == "OK" then
return maddr
end
return 0
end
function temp:getTemp()
if not self.init_done then
init()
end
return self:readIntByAddress(self.g_temp_ptr)
end
function temp:getTempFloat()
if not self.init_done then
init()
end
local res = self:readIntByAddress(self.g_temp_ptr)
if res ~= 0 then
return uint32_to_float(res)
end
return res
end
function temp:isEnabled()
if not self.init_done then
init()
end
local res = self:readIntByAddress(self.g_reminder_ptr)
return res == 0
end
function temp:enable()
if not self.init_done then
init()
end
memory:writeAddr(self.g_reminder_ptr, 0x00000000)
end
function temp:disable()
if not self.init_done then
init()
end
memory:writeAddr(self.g_reminder_ptr, 0x01010101)
end
return temp
Вот именно тут нам понадобятся адреса, которые получилось добыть в результате анализа прошивки, надеюсь, тут тоже все понятно, остается самый интересный момент - как происходит обращение к памяти, чтение и запись, а это и есть та самая киллер фича NuttX shell.
Модуль memory тоже является самописным, посмотрим же как там все устроено
memory.lua
local memory = {
tempFile = "/data/tmp_mem_".. os.date("%Y%m%d_%H%M%S")
}
local function reverse_uint32_bytes(n)
local b1 = (n >> 24) & 0xFF
local b2 = (n >> 16) & 0xFF
local b3 = (n >> 8) & 0xFF
local b4 = n & 0xFF
return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1
end
function memory:readAddr(addr, byteEndianIsLittle)
byteEndianIsLittle = byteEndianIsLittle or false
local hexAddr = string.format("%x", addr)
os.execute("mw 0x" .. hexAddr .. " > ".. self.tempFile)
for line in io.lines(self.tempFile) do
local value = string.match(line, "= 0x(%w+)")
if value then
dwordValue = tonumber(value, 16)
if byteEndianIsLittle then
dwordValue = reverse_uint32_bytes(dwordValue)
end
return dwordValue, "OK"
end
end
return 0, "ERR_FAIL"
end
function memory:readBytes(addr)
local hexAddr = string.format("%x", addr)
os.execute("mw 0x" .. hexAddr .. " > " .. self.tempFile)
for line in io.lines(self.tempFile) do
local value = string.match(line, "= 0x(%w+)")
if value then
-- Convert the 4-byte integer to a number
local intValue = tonumber(value, 16)
-- Split the 4-byte integer into individual bytes (little-endian order)
local byteArray = {}
for i = 0, 3 do
local byte = (intValue >> (i * 8)) & 0xFF
table.insert(byteArray, byte) -- Insert at the end for little-endian order
end
return byteArray, "OK"
end
end
return {}, "ERR_FAIL"
end
function memory:readUtf8String(addr)
local str = ""
local i = 0
while true do
-- Read 4 bytes (1 integer) at the current address
local bytes, status = self:readBytes(addr + i)
if status ~= "OK" then
return "", status
end
-- Process each byte in the 4-byte array
for _, byte in ipairs(bytes) do
if byte == 0 then -- Stop at the null terminator
return str, "OK"
end
str = str .. string.char(byte) -- Append the byte as a character
end
i = i + 4 -- Move to the next 4-byte chunk
end
end
function memory:writeAddr(addr, value)
local hexAddr = string.format("%X", addr)
local hexVal = string.format("%X", value)
os.execute("mw 0x" .. hexAddr .. "=" .. hexVal)
end
return memory
Дак вот для работы с памятью используется стандартная команда NuttX shell - mw
https://nuttx.apache.org/docs/latest/applications/nsh/commands.html#mb-mh-and-mw-access-memory
Вот ее подробная документация,
если кратко, то чтение выглядит так
mw 0x200A5150 4
>> 0x200A5150 = 0x00000000
запись так
mw 0x200A5150=0x01010101
Соответственно все что нам нужно, чтобы отключить функционал уведомлений и выключений
это сохранить по адресу переменной temp_reminder_in_progress значений не нуль,
что и делает метод temp:disable() в помощью модуля memory:
memory:writeAddr(self.g_reminder_ptr, 0x01010101)
А чтобы включить, то записать туда 0ль
memory:writeAddr(self.g_reminder_ptr, 0x00000000)
В итоге получился вот такой циферблат,
который прекрасно справляется в поставленной задачей

Как видите, очень интересная реализация системы позволяет управлять внутренними настройками и расширять границы функционала до реально потрясающих, даже без вмешательства в код прошивки.
В данном решение конечно же есть и недостаток, как понимаете, состояние RAM сбрасывается при перезагрузке или отключению системы, но так как данные часы могут работать по 2 недели, это не такой большой минус.
PS. Хочу сразу пояснить, что функционал температурного ограничения функционирования данных устройств лично для меня выглядит крайне разумным, но мне писали многие пользователи, так как на предыдущих моделях не испытывали таких проблем.
Я провел анализ часов, и выяснил что на текущий момент используются аккумуляторы Li-ion,
у которых рекомендуемый предел эксплуатации 55-60 гр, а пользователи которые обращались мне с проблемой мониторили температуру бенда, и она в топе оказалась 45 гр, соответственно требовалась всего небольшая коррекция температуры, чтобы он остался в рабочем состоянии.
Тем не менее, я рекомендую разумно относиться к теме безопасности, старайтесь избегать перегрева более 50 гр, ибо это чревато еще и деградацией батареи.
Подробнее с темой вы можете ознакомиться
Apache NuttX - https://nuttx.apache.org/
NuttShell - https://nuttx.apache.org/docs/latest/applications/nsh/index.html
LVGL - https://lvgl.io/
Easyface - https://github.com/m0tral/EasyFace
TempControl - https://github.com/m0tral/MiWatchLuaWatchfaces/tree/master/MiBand9/TempControl
Если вас интересует готовое решение, данные циферблаты готовы
и доступны в моей приложении MiFitness mod
в телеграмм канале @mi_watch_int @mi_watch_news
aegelsky
взрывом акума на руке и пожаром который невозможно потушить