- BrainTools - https://www.braintools.ru -

Я сделал экран для мониторинга моих квот LLM провайдеров

Начну с того, что еще недавно мои познания в схемотехнике балансировали на уровне непонимания различия между транзистором и резистором. Из всего арсенала, хоть как-то применимого к embedded, за душой был только Rust. Но когда на радаре замаячил так называемый встриваемый AI (машинное обучение [1] и компьютерное зрение [2] прямо на копеечных микроконтроллерах), – спортивный интерес [3] взял верх. Не тратя недели на теорию, я просто доехал до радиорынка и вернулся с джентльменским набором первопроходца: ESP32, первым попавшимся экраном и энкодером. Амбиции по запуску нейросетей моментально разбились о суровую реальность первого вечера: как вообще заставить этот кусок текстолита ожить и вывести на дисплей хотя бы пару цифр?

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

В итоге получился настольный монитор: нативное приложение не жрущее много памяти [4] для Windows, которое как раз собирает лимиты четырех подписок, и маленький экран с ESP32 показывающий их прямо перед глазами.

Железяка с 4 подписками

Железяка с 4 подписками

Что получилось

Система состоит из трёх частей:

Я сделал экран для мониторинга моих квот LLM провайдеров - 2
  • Windows-приложение получает квоты Claude, Codex, Grok и Kimi и отдаёт их в виде одного JSON.

  • ESP32 раз в 30 секунд забирает этот JSON по Wi-Fi и рисует его на экране. Энкодер переключает подписки и страницы.

  • Телефон открывает тот же адрес и видит адаптивную страницу.

    Окно приложения. Кнопки входа, выбор источника для Codex, обновление и трей. Сброс лимитов показан по UTC.

    Окно приложения. Кнопки входа, выбор источника для Codex, обновление и трей. Сброс лимитов показан по UTC.

Windows-приложение, прошивка ESP32 и общая структура данных между ними написаны на Rust (страница dashboard – обычные HTML и JS). Для меня это было главным аргументом начать: единственное, что я умел, должно было заработать с обеих сторон.

Железо: чему пришлось научиться

Список деталей короткий:

  • плата на ESP32 с USB-мостом CH340, в Windows она появилась как COM-порт;

  • цветной экран 1.8″ SPI, 128×160;

  • энкодер KY-040;

  • светодиод и резисторы.

Первая неожиданность возникла с экраном. Исходный план рассчитывал на распространенный контроллер ST7789, но на полученном модуле красовалась маркировка 1.8 TFT SPI 128×160 V1.1, по которой точную модель чипа определить невозможно. Методом проб и ошибок я выбрал драйвер ST7735S – это рабочее предположение, подтвержденное практикой. Шелкография на плате тоже преподнесла сюрприз: вместо привычных для SPI обозначений DC и MOSI там были A0 и SDA, так что распиновку пришлось сопоставлять с даташитами вручную.

Вторая неожиданность была приятной: транзисторы мне не понадобились вообще. Весь железный экспресс-курс свелся к правильному расчету резисторов:

  • подсветке экрана нужен токоограничивающий резистор номиналом порядка 220 Ом;

  • индикаторному светодиоду связи нужен свой резистор;

  • сигнальные входы нельзя подключать к 5 В, к счастью у ESP32 логика [5] 3,3 В.

    Экран TFT

    ESP32

    Назначение

    VCC, GND

    3V3, GND

    Питание модуля

    CS

    GPIO27

    Chip Select

    RESET

    GPIO25

    Сброс дисплея

    A0 (DC)

    GPIO26

    Data / Command

    SDA (MOSI)

    GPIO23

    Линия данных SPI

    SCK

    GPIO18

    Тактовая частота SPI

Энкодер подключён к GPIO2 (CLK), GPIO4 (DT) и GPIO16 (SW) с внутренними подтяжками. Отдельный светодиод на GPIO22 горит, когда нет связи с приложением на ПК.

Коннекторы на макетке

Коннекторы на макетке

Rust на ESP32: что выяснилось по пути

Прошивка крутится на esp-idf-svc и esp-idf-hal, то есть с полноценной поддержкой стандартной библиотеки std. За графику отвечают крейты embedded-graphics и драйвер mipidsi. Звучит прямолинейно, но сборка Rust под Xtensa на Windows с первого раза превращается в увлекательный квест:

  • Тулчейн. Для архитектуры Xtensa требуется отдельный форк компилятора Rust, который накатывается через утилиту espup. Дополнительно в системе должны стоять CMake, Ninja и Microsoft C++ Build Tools.

  • Генератор проекта. Свежий cargo-generate отказался собираться моим системным компилятором — пришлось явно фиксировать версию постарше.

  • Длинные пути. Сборка ESP-IDF в глубинах моих каталогов внезапно упала с фатальной ошибкой [6] Too long output directory и вежливой рекомендацией уложиться в десять символов. Инженерное решение выглядит забавно, но безотказно: корень сборки переехал в C:t.

  • Порт занят. Access is denied при прошивке означало не режим BOOT, а то, что старый монитор порта ещё открыт в другом окне терминала =).

  • BOOT. Периодический Failed to connect to ESP32 лечится классическим зажатием аппаратной кнопки BOOT на плате в момент инициализации соединения.

Когда пыль улеглась, плата успешно подняла Wi-Fi стек и приняла по сети первый JSON.

Зачем отдельное приложение на Windows

Соблазн стучаться в API провайдеров языковых моделей напрямую с ESP32 был велик, но от этой идеи я отказался почти сразу. Сложные цепочки OAuth2, постоянное TLS-рукопожатие, ротация refresh-токенов и парсинг четырех разнородных JSON-схем быстро сожрут скудные ресурсы микроконтроллера.

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

{
  "updated_at": "2026-10-29T12:17:16Z",
  "age_seconds": 8,
  "stale_after_seconds": 900,
  "providers": {
    "kimi": {
      "status": "ok",
      "source": "Kimi Code OAuth usage",
      "windows": [
        { "name": "5h", "remaining_percent": 0, "resets_at": "2026-10-29T10:30:54Z" }
      ]
    }
  }
}

Откуда берутся квоты

У каждого сервиса своя схема, и все они разные:

  • Claude отдаёт utilization, то есть использованную долю, поэтому остаток приходится вычислять инверсией. Учитываются скользящие окна на 5 часов и 7 дней, а также кастомные лимиты моделей.

  • Codex отдает структурированные окна с точной длительностью и таймстемпом сброса. Помимо прямого входа, я предусмотрел режим работы через локальный read-only балансировщик codex-lb.

  • Grok держит метрики внутри биллинга: недельный процент потребления либо месячную квоту при общем плане.

  • Kimi отдаёт окно на пять часов и месячные лимиты. Недельного окна у новых планов, как следует из документации, больше нет, поэтому экран показывает то, что реально пришло.

Чтобы интерфейс не вводил в заблуждение, я заложил четыре строгих правила обработки данных:

  1. Неизвестное не равно нулю. Если провайдер процент не сообщил, показывается --, а не 0%.

  2. Время сброса не выдумывается. Если его нет в ответе, на экране RST unknown.

  3. Устаревшее помечается. Если время сброса уже прошло или источник давно не отвечал, статус становится STALE, а цифры не подаются как свежие.

  4. Одно исключение, которое я ввёл сам. Если исчерпан глобальный недельный пул (7d = 0), остаток 5-часового окна принудительно зануляется (0%), поскольку фактически отправка запросов заблокирована до общего сброса недели.

Вход без паролей

Кнопки входа открывают системный браузер. Для Claude и Codex это авторизация с PKCE и возвратом на локальный порт, для Grok и Kimi device flow с кодом на странице. Пароль в окно я не ввожу никогда. Токены уходят в DPAPI, а новый refresh-токен записывается атомарно до следующего запроса: потерять его при ротации значит потерять вход.

Здесь была и честная неприятность. Claude уже отвечал мне 429, и приложение показывает это как ошибку, но сохраняет последние известные цифры помеченными как устаревшие. Чтобы это было видно сразу, строка провайдера с ошибкой запроса подсвечивается красным:

Codex переключён на недоступный пул: статус Error, строка красная. Остальные строки показывают AuthRequired, и красными они не бывают: отсутствие входа это не сбой запроса.

Codex переключён на недоступный пул: статус Error, строка красная. Остальные строки показывают AuthRequired, и красными они не бывают: отсутствие входа это не сбой запроса.

Экран и энкодер

На экране 160×128 помещаются четыре строки. Обзор строится только из тех провайдеров, которые реально пришли в ответе, без заготовленных пустых мест. Подключу пятого, он появится сам. Для каждого видны остатки первых двух окон, статус и время их сброса по UTC.

Нажатие энкодера открывает детали провайдера, вращение листает страницы окон и провайдеров:

Детали Kimi: источник, остатки и полное время сброса RST ММ-ДД ЧЧ:ММZ

Детали Kimi: источник, остатки и полное время сброса RST ММ-ДД ЧЧ:ММZ

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

const DELTAS: [i8; 16] = [0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0];

Кнопка декодера получила программный дебаунс на 20 мс, а сам опрос уехал в изолированный поток с интервалом в 1 мс чтобы не ронять системный стек Wi-Fi. После комфортного мира веб-разработки это отдельная ломка парадигмы: быстро выясняется, что ядра здесь – не абстрактные облачные vCPU, а вполне осязаемый кремний, где каждая задача должна тикать строго в своем ритме и на своем ядре, иначе плата мгновенно напомнит о себе паникой или таймером.

Dashboard в браузере

Тот же адрес открывается в браузере. Страница адаптивная и не загружает ничего со сторонних серверов, по умолчанию она сама обновляется раз в 30 секунд:

Каждая подписка отдельной карточкой с остатком и временем сброса. Всё, что видно на экране ESP32, видно здесь подробнее.

Каждая подписка отдельной карточкой с остатком и временем сброса. Всё, что видно на экране ESP32, видно здесь подробнее.

На широком экране карточки выстраиваются в ряд:

Каждая подписка отдельной карточкой с остатком и временем сброса. Всё, что видно на экране ESP32, видно здесь подробнее.

Каждая подписка отдельной карточкой с остатком и временем сброса. Всё, что видно на экране ESP32, видно здесь подробнее.

Сколько это весит

Мне хотелось, чтобы мое приложение не жрало системные ресурсы и просто весело в трее, поэтому десктопная часть написана на чистом Win32 API – без монструозных Electron, CEF или системных WebView, новомодных Tauri и бла-бла-бла. Замеры release-сборки с тремя активными подписками (Claude, Codex, Grok; до добавления Kimi):

  • память: около 17,9 МиБ working set и 2,8 МиБ private bytes;

  • CPU: за минуту наблюдения прирост составил 0 мс;

  • размер exe-шника: около 3,7 МиБ.

Естественно, нулевое потребление процессора фиксируется в покое – во время выполнения сетевых TLS-запросов и шифрования всплески активности есть, но они мгновенны и не заметны.

Для полноты картины приложение обзавелось собственной лаконичной иконкой в трее:

Иконочки

Иконочки

Что пошло не так

Остальные ограничения, о которых стоит помнить:

  • Эндпоинты неофициальные. Квоты читаются теми же адресами, что и агентные CLI. Они могут измениться или закрыться.

  • Правила сервисов. Сессии подписок переиспользуются сторонней утилитой. В документации того же Claude Code прямо указано, что OAuth-авторизация предназначена для их нативного софта, а интеграторам рекомендуется работать через классические API-ключи.

  • HTTP в локальной сети без шифрования. ESP32 забирает данные по открытому HTTP-протоколу. В пакетах передаются только проценты и даты без токенов и персональных данных, но выставлять такой сервис за пределы NAT не следует.

Что дальше

  • Докупить экран побольше!

  • Спроектировать и напечатать на 3D-принтере компактный корпус для рабочего стола.

Полный исходный код проекта, схемы соединений и пошаговое руководство по сборке опубликованы в репозитории: Тык [7].

Автор: AquaDll

Источник [8]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/37751

URLs in this post:

[1] обучение: http://www.braintools.ru/article/5125

[2] зрение: http://www.braintools.ru/article/6238

[3] интерес: http://www.braintools.ru/article/4220

[4] памяти: http://www.braintools.ru/article/4140

[5] логика: http://www.braintools.ru/article/7640

[6] ошибкой: http://www.braintools.ru/article/4192

[7] Тык: https://github.com/spgsroot/llm-quota-monitor

[8] Источник: https://habr.com/ru/articles/1101302/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1101302

www.BrainTools.ru

Rambler's Top100