Начну с того, что еще недавно мои познания в схемотехнике балансировали на уровне непонимания различия между транзистором и резистором. Из всего арсенала, хоть как-то применимого к embedded, за душой был только Rust. Но когда на радаре замаячил так называемый встриваемый AI (машинное обучение и компьютерное зрение прямо на копеечных микроконтроллерах), – спортивный интерес взял верх. Не тратя недели на теорию, я просто доехал до радиорынка и вернулся с джентльменским набором первопроходца: ESP32, первым попавшимся экраном и энкодером. Амбиции по запуску нейросетей моментально разбились о суровую реальность первого вечера: как вообще заставить этот кусок текстолита ожить и вывести на дисплей хотя бы пару цифр?
Цифры при этом были вполне конкретные. У меня сразу несколько подписок на LLM, и у каждой свои лимиты: окно на пять часов, окно на неделю, месячный потолок. Сбрасываются они в разное время, живут на разных сайтах, а узнать остаток можно, только открыв нужную вкладку. Мне хотелось поднять глаза от клавиатуры и сразу увидеть, на что еще хватит квоты.
В итоге получился настольный монитор: нативное приложение не жрущее много памяти для Windows, которое как раз собирает лимиты четырех подписок, и маленький экран с ESP32 показывающий их прямо перед глазами.
Что получилось
Система состоит из трёх частей:

-
Windows-приложение получает квоты Claude, Codex, Grok и Kimi и отдаёт их в виде одного JSON.
-
ESP32 раз в 30 секунд забирает этот JSON по Wi-Fi и рисует его на экране. Энкодер переключает подписки и страницы.
-
Телефон открывает тот же адрес и видит адаптивную страницу.
Окно приложения. Кнопки входа, выбор источника для 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 логика 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 в глубинах моих каталогов внезапно упала с фатальной ошибкой
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 отдаёт окно на пять часов и месячные лимиты. Недельного окна у новых планов, как следует из документации, больше нет, поэтому экран показывает то, что реально пришло.
Чтобы интерфейс не вводил в заблуждение, я заложил четыре строгих правила обработки данных:
-
Неизвестное не равно нулю. Если провайдер процент не сообщил, показывается
--, а не 0%. -
Время сброса не выдумывается. Если его нет в ответе, на экране
RST unknown. -
Устаревшее помечается. Если время сброса уже прошло или источник давно не отвечал, статус становится
STALE, а цифры не подаются как свежие. -
Одно исключение, которое я ввёл сам. Если исчерпан глобальный недельный пул (7d = 0), остаток 5-часового окна принудительно зануляется (0%), поскольку фактически отправка запросов заблокирована до общего сброса недели.
Вход без паролей
Кнопки входа открывают системный браузер. Для Claude и Codex это авторизация с PKCE и возвратом на локальный порт, для Grok и Kimi device flow с кодом на странице. Пароль в окно я не ввожу никогда. Токены уходят в DPAPI, а новый refresh-токен записывается атомарно до следующего запроса: потерять его при ротации значит потерять вход.
Здесь была и честная неприятность. Claude уже отвечал мне 429, и приложение показывает это как ошибку, но сохраняет последние известные цифры помеченными как устаревшие. Чтобы это было видно сразу, строка провайдера с ошибкой запроса подсвечивается красным:
Error, строка красная. Остальные строки показывают AuthRequired, и красными они не бывают: отсутствие входа это не сбой запроса.Экран и энкодер
На экране 160×128 помещаются четыре строки. Обзор строится только из тех провайдеров, которые реально пришли в ответе, без заготовленных пустых мест. Подключу пятого, он появится сам. Для каждого видны остатки первых двух окон, статус и время их сброса по UTC.
Нажатие энкодера открывает детали провайдера, вращение листает страницы окон и провайдеров:
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 секунд:
На широком экране карточки выстраиваются в ряд:
Сколько это весит
Мне хотелось, чтобы мое приложение не жрало системные ресурсы и просто весело в трее, поэтому десктопная часть написана на чистом 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-принтере компактный корпус для рабочего стола.
Полный исходный код проекта, схемы соединений и пошаговое руководство по сборке опубликованы в репозитории: Тык.
Автор: AquaDll


