Обзор Divoom Timegate: перспективы и проблемы устройства - Академия Selectel

Обзор Divoom Timegate: перспективы и проблемы устройства

Владимир Туров
Владимир Туров Разработчик
18 декабря 2025

Изображение записи

В рекомендациях одного маркетплейса мне попалось чудо китайской киберпанковой мысли — часы и по совместительству пиксельный дисплей Divoom Timegate. Мне понравился визуальный стиль и потенциальная возможность выводить любую информацию. У часов есть свое приложение с регистрацией, что навевает грустные мысли… Вдруг часы управляются исключительно через серверы производителя? Я купил эти часы и немного разобрался в их интерфейсах.

Краткий обзор

Фото часов.
Фото часов.
Задняя панель часов.
Задняя панель часов. Источник.

В рунете есть полноценный текстовый обзор этого устройства за авторством Максима Романова, я же остановлюсь только на основных характеристиках. Но начнем с определения.

Divoom Timegate — это умные часы на базе ESP32 с пятью LCD-дисплеями 1.44” разрешением 128×128 пикселей и подсветкой корпуса и пространства за часами. 

Управлять устройством можно с помощью пяти физических кнопок на верхней части устройства, либо через официальное приложение по Wi-Fi 2,4 ГГц. На момент написания статьи устройство стоит 8 000 рублей.

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

Луч надежды есть: пользователь может выводить информацию о своем компьютере на свои часы. Но насколько быстро она обновляется? Задокументировано ли API? Обязательно ли подключать часы к интернету? Давайте выясним.

Анализ поведения

Меню выбора циферблатов.
Меню выбора циферблатов.

Часы абсолютно точно умеют выходить в сеть самостоятельно, потому что стандартные циферблаты предлагают много информации, которая доступна только через интернет и требует периодического обновления. Это может быть RSS-фид, курсы валют, погода и информации, извлеченная из других сервисов типа Huawei Health.

При каждом включении часы устанавливают соединение с сервером api.divoom-gz.com и обмениваются данными по MQTT и HTTPS. Устройство при загрузке отправляет на сервер некоторый набор данных о себе.

  • Локальный IP-адрес, который назначен часам.
  • Внешний IP-адрес в интернете.
  • Идентификатор устройства.
  • Настройки погоды — широта и долгота, которые настраиваются пользователем в приложении и сохраняются в памяти часов.

В остальное время часы отправляют на сервер heartbeat-запросы раз в 15 секунд и, в зависимости от виджетов, направляют HTTP-запросы к эндпоинтам виджетов. Отправку всех этих данных можно пресечь правилами брандмауэра. Часы могут работать в автономном режиме, в том числе без первоначальной «активации» — достаточно нажать кнопку включения, когда устройство подключается к Wi-Fi. 

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

Приложение на телефон отправляет запросы на сервер divoom, а управление часами происходит через MQTT-команды от сервера к часам. Таким образом, часами можно управлять без подключения к локальной сети и даже давать доступ к часам другим аккаунтам. Поэтому если запретить устройству доступ в интернет, то управление через приложение сломается. Однако все еще возможен «костыль»: можно запрещать доступ в интернет после полной загрузки часов. Тогда локальные интерфейсы будут доступны, приложение не будет работать, а на душе будет немного спокойнее.

На часах есть микрофон, который используется для визуализации громкости звуков вокруг устройства на некоторых циферблатах. За время исследования не появилось подозрения, что звук с устройства передается в интернет.

В целом, интернет-интерфейсы часов меня более-менее устраивают. Время изучить способы создания циферблатов.

Управление в локальной сети

Как отмечалось ранее, у Divoom Timegate есть локальные интерфейсы, но активируются они только после подключения к интернету. Возможно, это случайная недоработка, а может быть — намеренное решение разработчиков.

Интересно, что для получения всех устройств в локальной сети достаточно сделать запрос к серверам Divoom и узнать адреса всех устройств Divoom, которые вышли в интернет с тем же внешним адресом:


      $ curl -XPOST https://app.divoom-gz.com/Device/ReturnSameLANDevice | jq
{
  "ReturnCode": 0,
  "ReturnMessage": "",
  "DeviceList": [
    {
      "DeviceName": "Times Gate",
      "DeviceId": число,
      "DevicePrivateIP": "192.168.1.182",
      "DeviceMac": "0123456789ab",
      "Hardware": 400
    }
  ]
}

В локальной сети у Timegate нет никаких дополнительных методов аутентификации и разграничения доступа. Для управления достаточно знать адрес в локальной сети и отправлять правильные команды. Команды задокументированы на отдельной странице. Для комфортной работы нужно принять следующие факты.

  1. Все команды к часам — это JSON-объект, отправляемый POST-запросом на адрес http://DevicePrivateIP/post, где DevicePrivateIP — локальный адрес часов.
  2. Если команда сформирована правильно, то ответ — это JSON-объект, где в поле error_code будет число 0. Если команда некорректна, то в ответе вместо числа увидим строку «Request data illegal json». Эта ошибка будет вне зависимости от исходной причины.
  3. Документация местами говорит загадками. Более того, некоторые функции описаны как «будет доступно начиная с версии X». При этом посмотреть версию ПО на часах нельзя. 🙂
  4. Некоторые команды требуют идентификаторов, которые можно получить только от API Divoom. Но на момент написания статьи нужные эндпоинты возвращают пустые списки. Остается использовать «метод научного тыка», то есть перебор возможных значений.
  5. В документации много опечаток, в том числе в параметрах. Несмотря на это, документация точна: устройство ожидает имена параметров ровно так, как они написаны в документации.
  6. Часы работают с Gif с разрешением 16×16, 32×32, 64×64, 128×128. Загрузка Gif-изображений возможна с HTTP-сервера. Один из эндпоинтов позволяет загружать анимации покадрово в формате JPEG в виде base64-строки.  

С самого начала мне пригляделся метод с названием «Send display list», который позволяет за один запрос загрузить картинку на фон и создать текстовые поля. Именно в этом методе есть опечатка BackgroudGif

Герои игры Baldur’s Gate в разрешении 128x128.
Герои игры Baldur’s Gate в разрешении 128×128. Источник.

Конвертируем изображения в формат GIF разрешением 128х128 пикселей и загружаем на любой удобный веб-сервер. Затем готовим полезную нагрузку. Можно использовать curl, но мы двигаемся к автоматизации, поэтому я воспользуюсь языком программирования Python и библиотекой requests. 


      import requests

names = ["astarion", "gale", "karlach", "laezel", "shadowheart"]

for i, name in enumerate(names):
    lcd_payload = {
        "Command": "Draw/SendHttpItemList",
        # Индекс дисплея 0-4
        "LcdIndex": i,
        # Булев флаг полного обновления экрана
        # Если 1, то картинка обновляется
        "NewFlag": 1,
        # Ссылка на картинку на локальном веб-сервере
        "BackgroudGif": f"http://192.168.1.109:3380/images/{name}.gif",
        # Описание текстов на
        "ItemList": [
            {
                # Уникальный идентификатор текстового поля,
                # по котором потом можно менять текст
                "TextId": 10 + 2 * i,
                # Тип "пользовательский текст"
                "type": 22,  # DIVOOM_DISP_CUSTOM_DIAL_SUPPORT_TEXT_MESSAGE
                # Абсолютное положение текста на экране
                "x": 50,
                "y": 110,
                # Направление прокрутки текста, если не вмещается
                "dir": 0,  # 0 - LEFT, 1 - RIGHT
                # Идентификатор шрифта. См. ниже
                "font": 4,
                # Ширина и высота текстового поле
                "TextWidth": 128,
                "Textheight": 16,
                # Строка для отображения
                "TextString": name,
                # Скорость прокрутки
                "speed": 100,
                # Цвет текста
                "color": "#FFFFFF",
            }
        ]
    }

    response = requests.post("http://192.168.1.182/post", json=lcd_payload)
    response.raise_for_status()
    print(response.json())

Теперь можно оценить быстродействие обновления дисплеев по командам. Обновление всех дисплеев заняло около шесть секунд от первого запроса до полной загрузки.

Процесс отображения изображений на дисплеях.
Запросы были сделаны последовательно. Отображение происходит медленнее и в другом порядке.

С идентификатором шрифта возникли проблемы. Документация предлагает посетить страницу, но на момент написания статьи на ней заглушка «TEMPORARILY CLOSED!», а вызов к API возвращает пустой список. 

Шрифты, которые были найдены перебором.
Шрифты, которые были найдены перебором.

К счастью, идентификатор шрифта — это натуральное число. Я перебрал значения от 1 до 44, выводя строку «font %d шрифт» на экран. Таким образом обнаружились три алфавитных шрифта с поддержкой латиницы, кириллицы и китайских иероглифов (2, 4, 32), шесть числовых шрифтов с поддержкой спецсимволов погоды (18, 20, 22, 26, 28, 42) и четыре числовых шрифта (24, 30, 36, 44). При этом размер шрифта изменить нельзя. 

Однако обновлять текст на экране не так просто, как ожидается. Можно отправить тот же запрос, установив NewFlag в значение 0, чтобы не обновлять фоновую картинку. Но нас ждет неприятный сюрприз: смена текста без смены картинки тоже приводит к появлению песочных часов с надписью «Loading». Да, экран обновляется гораздо быстрее, но это неприятных побочный эффект.

Я нашел единственный способ: вместо статического текста использовать «интернет-текст». Полезная нагрузка в таком случае выглядит так:


      lcd_payload = {
    "Command": "Draw/SendHttpItemList",
    "LcdIndex": lcdIndex,
    "NewFlag": 1,
    "BackgroudGif": f"http://{LISTEN_ADDRESS}:{LISTEN_PORT}/images/{char}.gif",
    "ItemList": [
        {
            "TextId": 1 + 2 * lcdIndex,
            "type": 23, # DIVOOM_DISP_CUSTOM_DIAL_SUPPORT_NET_TEXT_MESSAGE ,
            "x": 50,
            "y": 110,
            "dir": 0, # LEFT
            "font": 4,
            "TextWidth": 128,
            "Textheight": 16,
            "TextString": f"http://{LISTEN_ADDRESS}:{LISTEN_PORT}/text/{i}",
            "speed": 100,
            "color": "#FFFFFF",
            "update_time": 1
        }
    ]
}

Обратите внимание, что в TextString передается URL, по которому часы будут запрашивать строку для выведения раз в update_time секунд. Часы ожидают JSON-ответ, в котором ищут и выводят строковое поле `DispData`. Вот так можно создать веб-сервер, удовлетворяющий условиям с помощью FastAPI.


      import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/text/{lcd_index}")
async def text_for_lcd(lcd_index: int):
    return {"DispData": f"lcd {lcd_index}"}

if __name__ == "__main__":
    uvicorn.run(app, host=”192.168.1.109, port=3380)

Таким образом, нужно один раз загрузить все текстовые поля через API-запрос Draw/SendHttpItemList, а потом управлять содержимым через веб-сервер, который отдает актуальные данные. Часы автоматически обновляют текстовое поле без загрузочного экрана.

С основами разобрались — теперь сделаем интеграцию с Baldur’s Gate 3 и оценим решение на реальной задаче.

Интеграция с Baldur’s Gate 3

Официальные инструменты для создания модов в Baldur’s Gate 3 появились в сентябре 2024 года вместе с седьмым патчем. Это довольно обширная тема, но я расскажу о минимальных шагах, которые нужны для создания интеграции. Моя интеграция будет экспортировать состояние группы на часы: аватарки персонажей, а также их здоровье. 

Сперва соберем все инструменты.

  • Baldur’s Gate 3. Очевидно, нужна сама игра. Я использую Steam-версию.
  • Baldur’s Gate 3 Toolkit Data. Это бесплатное дополнение для основной игры, которое приносит необходимые ресурсы для редактора.
  • Baldur’s Gate 3 Toolkit. Появляется отдельной программой в «Инструментах» в Steam после покупки игры. Это непосредственно редактор модов. При первом запуске нужно указать каталог с игрой.
  • Norbyte’s Baldur’s Gate 3 Script Extender. Пользовательское расширение, добавляющее Lua-движок в игру и совместимое с официальной системой модов. 

Для отладки и тестирования включаем Lua-консоль для игры: в каталоге steamapps\common\Baldurs Gate 3\bin создаем файл ScriptExtenderSettings.json и заполняем его следующим содержимым:


      {
  "CreateConsole": true
}

Открываем редактор и создаем мод — я назвал его PartyExporter. Обратите внимание, что «проект» мода в редакторе хранится по адресу steamapps\common\Baldurs Gate 3\Data\Projects\PartyExporter_9c02cff0-eb23-af65-4d43-aa810b27566b, а реальные файлы, загруженные в игру, — в соседнем каталоге: steamapps\common\Baldurs Gate 3\Data\Mods\PartyExporter_9c02cff0-eb23-af65-4d43-aa810b27566b. Если вы хотите изменять свой скрипт в игре и использовать его функции в Lua-консоли, то необходимо работать с файлами, загруженными в игру. И иногда сохранять свою работу в проект.

Script Extender дает доступ ко внутренним функциям игры, но, как и следует внутриигровому скриптовому движку, не дает работать с сетью вне протокола. Поэтому скрипт внутри игры должен решать ровно одну задачу: извлекать данные из нее и записывать в файл, доступный всем остальным программам.

На словах идея проста.

  1. Сделать функцию, которая экспортирует состояние группы в файл: идентификаторы персонажей и их состояние здоровья (текущее и максимальное).
  2. Вызывать функцию из п.1 при событиях получения урона или каким-то иным образом.

На деле особенности игры внесли свои коррективы. Игра состоит из сервера и клиента. Даже когда вы играете в одиночную игру, то «под капотом» у вас локально поднимается сервер игры, к которому вы подключаетесь. С точки зрения мода это два независимых контекста. 

Создаем каталог ScriptExtender, конфигурацию и скрипт BootstrapServer.lua, который выполняется в серверном контексте при загрузке игровой сессии. Структура файлов внутри каталога мода должна выглядеть так:


      PartyExporter_9c02cff0-eb23-af65-4d43-aa810b27566b
├── GUI
│   └── metadata.lsf
├── Localization
│   └── English
├── ScriptExtender
│   ├── Config.json
│   └── Lua
│       └── BootstrapServer.lua
└── meta.lsx

Я не смог найти события, которые происходят при получении урона персонажами. Но я нашел событие «Игровой такт». 

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


      local accumulator = 0
local interval = 1.0 -- секунды

function MyPeriodicFunction()
    _P("Прошла 1 секунда")
end

Ext.Events.Tick:Subscribe(function(e)
    accumulator = accumulator + e.Time.DeltaTime

    if accumulator >= interval then
        accumulator = accumulator - interval
        MyPeriodicFunction()
    end
end)

В серверном контексте доступны функции из игрового интерфейса Osiris, через который можно контролировать игровой процесс. В частности, там есть база данных игровой компании — DB_PartyMembers. Для уменьшения I/O-нагрузки записываем файл не каждую секунду, а только в случае изменений здоровья. Таким образом, файл будет перезаписываться только во время боев.


      local payload = {}

function MyPeriodicFunction()
    local update = false
    -- Получаем участников группы
    local party = Osi.DB_PartyMembers:Get(nil)
    
    -- Если обновилось количество игроков в группе, то обновляем всё
    if #party ~= #payload then
        update = true
        payload = {}
        for i, _ in pairs(party) do
            payload[i] = {
                name = "4",
                hp = 0,
                max_hp = 0,
                temp_hp = 0,
                max_temp_hp = 0,
            }
        end
    end
    
    for i, v in pairs(party) do
        -- В базе данных только UUID сущностей. Получаем данные по UUID
        local entity = Ext.Entity.Get(v[1])
        if payload[i].name ~= v[1] then
            payload[i].name = v[1]
            update = true
        end
        if payload[i].hp ~= entity.Health.Hp then
            payload[i].hp = entity.Health.Hp
            update = true
        end
        if payload[i].max_hp ~= entity.Health.MaxHp then
            payload[i].max_hp = entity.Health.MaxHp
            update = true
        end
        if payload[i].temp_hp ~= entity.Health.TemporaryHp then
            payload[i].temp_hp = entity.Health.TemporaryHp
            update = true
        end
        if payload[i].max_temp_hp ~= entity.Health.MaxTemporaryHp then
            payload[i].max_temp_hp = entity.Health.MaxTemporaryHp
            update = true
        end
    end
    -- Если хоть один параметр изменился - записываем в файл
    if update == true then
        _D(payload)
        Ext.IO.SaveFile("output.json", Ext.DumpExport(payload))
    end
end

Функция Ext.IO.SaveFile записывает файл в каталог %localappdata%\Larian Studios\Baldur's Gate 3\Script Extender. В нем все очевидно, кроме имен персонажей. В файл попадает не человекочитаемое имя, а служебный идентификатор. Впрочем, довольно легко угадать, что у меня главный герой — Карлах:


      [
    {
        "hp": 177,
        "max_hp": 190,
        "max_temp_hp": 0,
        "name": "S_Player_Karlach_2c76687d-93a2-477b-8b18-8a14b549304c",
        "temp_hp": 0
    }
]

Осталось написать веб-сервер, который будет считывать файл и настраивать часы. Не буду разбирать код целиком, отмечу только основные моменты.

  1. Используется FastAPI с примонтированным каталогом /images для статических файлов.
  2. При запуске веб-сервера сохраняется выбранный часами циферблат.
  3. Веб-сервер считывает файл раз в секунду. Если файл пуст, то восстанавливается оригинальный циферблат. А если количество компаньонов отличается, то циферблат формируется заново.

Получилось так. Ссылка на исходный код проекта в конце статьи.

Можно заметить, что между действием в игре и отображением на часах есть небольшая задержка. Но в пошаговой игре это простительно. А еще «зависает» подсветка из-за слишком частых запросов.

Что не работает

Отдельно хочу в одном месте описать все, что нельзя сделать с часами на данный момент через доступные интерфейсы.

  • Версию прошивки устройства посмотреть нельзя.
  • В документации есть упоминания, что некоторые функции работают только с версии 90102. В частности, пакетное выполнение команд, выравнивание текста по центру или по правому краю. Пакетное выполнение команд сработало, а выравнивание текста — нет.
  • Список шрифтов получить не удалось.
  • «Самодельные» циферблаты могут отображать изображение и текст. Шкалы прогресса сделать нельзя. 
  • Если запросов в «самодельном» циферблате слишком много или они слишком частые, то часы не уделяют времени подсветке и она «зависает».

Заключение

Divoom Timegate может быть довольно сложным и капризным устройством, если вы планируете подключить его к системам умного дома или каким-то игровым интеграциям. Однако если вы не боитесь сложностей, хотите решать загадки, которые до вас никто не решал, и вам нравится визуальный стиль устройства, то вы не пожалеете. По крайней мере, я не жалею о совершенной покупке.

Ознакомиться с полным обзором Максима Романова можно здесь. Исходный код проекта доступен по ссылке.