Skip to content

Latest commit

 

History

History
2302 lines (1717 loc) · 93.5 KB

File metadata and controls

2302 lines (1717 loc) · 93.5 KB

PipKit API

Этот файл описывает актуальный публичный API PipGUI и PipCore, который есть в коде проекта.

Требования к компиляции (PlatformIO)

PipKit использует C++17. Для PlatformIO добавьте в platformio.ini:

build_unflags =
    -std=gnu++11
build_flags =
    -std=gnu++17

1. Build-time флаги

Низкий слой PipCore выбирает платформу, драйвер дисплея и optional backend-модули на этапе компиляции. Эти флаги задаются в include/config.hpp.

  • PIPCORE_DISPLAY
    • пример: #define PIPCORE_DISPLAY ST7789
    • поддерживаемые дисплеи: ST7789, ILI9488, SIMULATOR
  • PIPCORE_PLATFORM
    • пример: #define PIPCORE_PLATFORM ESP32
    • поддерживаемые платформы: ESP32, DESKTOP
  • PIPCORE_ENABLE_PREFS
    • 0 или 1
  • PIPCORE_ENABLE_WIFI
    • 0 или 1
  • PIPCORE_ENABLE_OTA
    • 0 или 1
  • PIPCORE_OTA_PROJECT_URL
    • базовый OTA URL для core-level OTA backend

Пример через include/config.hpp:

#define PIPCORE_PLATFORM ESP32
#define PIPCORE_DISPLAY ST7789
#define PIPCORE_ENABLE_PREFS 1
#define PIPCORE_ENABLE_WIFI 1
#define PIPCORE_ENABLE_OTA 1
#define PIPCORE_OTA_PROJECT_URL "https://example.com/fw/PipGUI"

Пример для ILI9488:

#define PIPCORE_PLATFORM ESP32
#define PIPCORE_DISPLAY ILI9488
#define PIPCORE_ENABLE_PREFS 1
#define PIPCORE_ENABLE_WIFI 1
#define PIPCORE_ENABLE_OTA 1
#define PIPCORE_OTA_PROJECT_URL "https://example.com/fw/PipGUI"

Что важно:

  • по умолчанию optional-модули PipCore выключены
  • если внешний код использует pipcore::net::* при PIPCORE_ENABLE_WIFI=0, сборка падает на этапе компиляции
  • если внешний код использует pipcore::ota::* при PIPCORE_ENABLE_OTA=0, сборка тоже падает на этапе компиляции
  • это намеренное поведение: выключенный модуль нельзя использовать "молча"
  • PIPGUI_* и PIPCORE_* это разные слои конфигурации: GUI и Core соответственно
  • если PipGUI реально использует Wi-Fi / OTA, соответствующие PIPCORE_ENABLE_* тоже должны быть включены явно

2. Desktop simulator

Симулятор нужен для локального прогона GUI на Windows/Linux. Он собирает проект в desktop-конфигурации и подменяет platform/display слой на DESKTOP + SIMULATOR.

Windows:

powershell -ExecutionPolicy Bypass -File .\Tools\Simulator\Windows\Sim.ps1

Linux:

./Tools/Simulator/Linux/Sim.sh

Полезные варианты:

powershell -ExecutionPolicy Bypass -File .\Tools\Simulator\Windows\Sim.ps1 -Debug
powershell -ExecutionPolicy Bypass -File .\Tools\Simulator\Windows\Sim.ps1 -NoRun
powershell -ExecutionPolicy Bypass -File .\Tools\Simulator\Windows\Sim.ps1 -InstallDeps
./Tools/Simulator/Linux/Sim.sh --debug
./Tools/Simulator/Linux/Sim.sh --no-run
./Tools/Simulator/Linux/Sim.sh --install-deps

Что делает:

  • при Debug собирает debug-вариант
  • при NoRun / --no-run только собирает исполняемый файл без запуска
  • если wxWidgets ещё не установлен в .sim/deps, Windows скрипт сам вызывает Tools/Simulator/Windows/Sim-Deps.ps1

/Sim-Deps.ps1 и /Sim-Deps.sh это отдельная подготовка simulator-зависимостей:

  • Windows: ставит/обновляет локальный vcpkg в .sim/deps/vcpkg, ставит wxwidgets:x64-windows-release
  • Linux: ставит системные пакеты через доступный менеджер пакетов (apt, dnf, pacman, zypper), включая CMake/Ninja/wxWidgets/ffmpeg

Что важно по окружению:

  • Windows использует Visual Studio C++ toolchain и wxWidgets из локального vcpkg
  • Linux использует системный C++ toolchain, CMake/Ninja и системный wxWidgets
  • для simulator build PipCore сначала пытается подключить config_sim.hpp, а если его нет, использует обычный config.hpp
  • host-обвязка simulator-а живёт внутри PipCore/Host/Desktop/

2.1. Build-time флаги simulator

  • PIPGUI_SIM_SCALE
    • масштаб окна simulator относительно логического framebuffer
    • по умолчанию 1
  • PIPGUI_SIM_PROJECT_NAME
    • опциональное название проекта для заголовка окна
    • пример: #define PIPGUI_SIM_PROJECT_NAME "Roma"
    • если не задано, заголовок будет просто Simulator
  • PIPGUI_SIM_DEFAULT_WIDTH
    • fallback-ширина экрана симулятора, если проект не вызвал configDisplay().size(...)
    • по умолчанию 480
  • PIPGUI_SIM_DEFAULT_HEIGHT
    • fallback-высота экрана симулятора, если проект не вызвал configDisplay().size(...)
    • по умолчанию 320
  • PIPGUI_SIM_BTN_PREV_PIN
    • какой виртуальный пин считать кнопкой Prev
    • по умолчанию 4
  • PIPGUI_SIM_BTN_NEXT_PIN
    • какой виртуальный пин считать кнопкой Next
    • по умолчанию 20
  • PIPGUI_SIM_BTN_SELECT_PIN
    • какой виртуальный пин считать кнопкой Select
    • по умолчанию 21

Пример:

#define PIPCORE_PLATFORM DESKTOP
#define PIPCORE_DISPLAY SIMULATOR

#define PIPGUI_SIM_SCALE 2
#define PIPGUI_SIM_PROJECT_NAME "Roma"
#define PIPGUI_SIM_DEFAULT_WIDTH 320
#define PIPGUI_SIM_DEFAULT_HEIGHT 240
#define PIPGUI_SIM_BTN_PREV_PIN 16
#define PIPGUI_SIM_BTN_NEXT_PIN 9
#define PIPGUI_SIM_BTN_SELECT_PIN 21

2.2. Управление в окне simulator

  • Left или A - кнопка Prev
  • Right или D - кнопка Next
  • Enter или Space - кнопка Select
  • клавиши 0..3 отправляются в serial input simulator-а как обычные символы

В правой панели доступны настройки:

  • пауза/возобновление
  • шаг кадра вперёд и назад, с настраиваемым количеством кадров за один шаг
  • Time scale slider для замедления/ускорения simulator-clock
  • SPI bottleneck с частотами от 27 до 80 MHz;
  • RGB565 preview для просмотра как на 16-bit дисплее
  • Создание скриншотов в .sim/shots/ (PNG, если доступен, BMP, если обработчик изображений недоступен)
  • Запись MP4 видео в .sim/videos/ через ffmpeg
  • перезапуск симулятора
  • встроенная консоль для логирования снизу окна

Под экраном рендера simulator показывает компактные метрики:

  • Redrawn - процент и количество пикселей, перерисованных за последний показанный кадр
  • Heap / Peak — приблизительный трекер SRAM для оценки дефицита памяти на стороне устройства
  • Render CPU - время подготовки последнего кадра на стороне процессора
  • SPI estimate - расчётное время передачи изменённых пикселей на текущей SPI частоте

Ограничения и отличия от железа:

  • Это десктопная среда выполнения, а не потактный эмулятор ESP32
  • SPI throttling моделирует пропускную способность передачи данных на дисплей, но не эмулирует периферию DMA/SPI потактово
  • Тайминги, ввод и системные бэкенды могут отличаться от реального устройства
  • Скриншот/видео hotkeys simulator-а работают отдельно от встроенной скриншот-системы PipGUI
  • в simulator config по умолчанию выключены PIPGUI_WIFI, PIPGUI_OTA и built-in screenshots, даже если core-level backend оставлен включённым для совместимости сборки

2.3. Что нужно проекту для симуляции

Если проект пишет графику через PipCore / PipGUI, для simulator обычно достаточно следующего:

  • config_sim.hpp, где выбраны PIPCORE_PLATFORM DESKTOP и PIPCORE_DISPLAY SIMULATOR
  • host include path, чтобы компилятор видел PipCore/Host/Desktop compat-layer
  • desktop build script или CMake, который собирает:
    • код PipCore
    • код самого проекта
    • PipCore/Host/Desktop/src/Runner.cpp
    • PipCore/Host/Desktop/src/Globals.cpp

Что важно:

  • если UI-код опирается только на PipCore / PipGUI, этого достаточно для графической симуляции
  • если проект использует прямые ESP32-only вызовы, для них всё равно нужны desktop stub/compat-реализации или абстракция через PipCore

3. Инициализация

3.1. Конфигурация дисплея

Полный пример:

ui.configDisplay()
    .pins(11, 12, 10, 9, -1)   // mosi, sclk, cs, dc, rst
    .size(240, 320)            // width, height
    .hz(80000000)              // частота SPI
    .order("RGB")              // "RGB" или "BGR"
    .invert(true)              // инверсия панели
    .swap(false)               // swap байтов RGB565
    .offset(0, 0);             // смещение активной области

Минимальный пример:

ui.configDisplay()
    .pins(11, 12, 10, 9, -1)
    .size(240, 320);

Можно не писать и оставить display-specific дефолты core/drivers:

  • hz(freq):
    • ST7789 по умолчанию 80000000
    • ILI9488 по умолчанию 60000000
  • order("RGB"):
    • ST7789 по умолчанию RGB
    • ILI9488 по умолчанию BGR
  • invert(bool) по умолчанию true
  • swap(bool) по умолчанию false
  • offset(x, y) по умолчанию (0, 0)

Что важно:

  • если hz(...) или order(...) не заданы, используются именно дефолты выбранного драйвера в PipCore
  • это особенно важно для ILI9488, потому что его рабочие дефолты отличаются от ST7789

Обязательно указать:

  • pins(...) задаёт пины SPI и управляющие пины дисплея.
  • size(width, height) задаёт логический размер панели.

Пример для ILI9488:

ui.configDisplay()
    .pins(11, 12, 10, 9, 14)
    .size(320, 480);

3.2. Запуск GUI

После конфигурации дисплея вызывается begin():

ui.begin(0);        // rotation: 0..3
// ui.begin(0, true);  // forceTiles: принудительно включить tiled-mode (2 горизонтальных тайла)
  • rotation принимает значения 0..3.
  • forceTiles=true включает tiled-mode даже если хватает памяти на полный screen-buffer.
  • если памяти не хватает для full canvas, библиотека автоматически уходит в tiled-mode.
  • при внутреннем GUI OOM библиотека сначала освобождает временные буферы и кэши, повторяет выделение и, если full canvas удержать всё равно нельзя, бесшовно деградирует в tiled-mode.
  • auto-tiled режим не является постоянным: когда runtime снова может выделить full canvas в спокойном кадре, он возвращает fullscreen rendering. forceTiles=true остаётся принудительным tiled-mode и обратно не повышается.
  • стартовый фон GUI фиксированно чёрный (0x0000).

Ограничения tiled-mode:

  • screen-transition анимации отключены (требуют полного screen-buffer).
  • dirty-redraw для повёрнутого tiled-frame сейчас не делается: при изменениях rotated tiled-screen перерисовывается целиком, но только по реальному запросу redraw, а не постоянно каждый тик.

Проверить текущий режим можно через ui.tiledMode().

Runtime-поворот экрана

ui.setRotation(0);        // повернуть экран сразу или с анимацией по умолчанию
ui.setRotation(1, 620);   // тот же поворот, но со своей длительностью

ui.screenRotation();              // текущая ориентация 0..3
ui.rotationTransitionActive();    // идёт ли сейчас анимация переворота
  • setRotation(...) меняет ориентацию уже после begin(), без повторного configDisplay()
  • библиотека держит физическую конфигурацию дисплея как есть и поворачивает GUI логически, пересобирая внутренний sprite под новый размер
  • screenRotation() возвращает текущий runtime rotation 0..3
  • rotationTransitionActive() позволяет не запускать свой второй переход поверх уже идущего переворота

3.3. Подсветка и яркость

ui.setBacklight()
    .pin(48)          // pin
    .channel(0)       // PWM channel
    .freq(5000)       // PWM frequency
    .resolution(12);  // PWM resolution bits

Что важно:

  • pin - обязательный
  • channel, frequency и resolution можно не указывать
  • после setBacklight(...) boot-анимация LightFade уже сама управляет яркостью
  • если backlight не настроен, LightFade управлять нечем

Максимальная яркость

ui.setMaxBrightness(70);   // ограничить максимум 70%
  • диапазон 0..100
  • это верхний лимит яркости, библиотека использует этот лимит, когда сама меняет яркость
  • на текущей ESP32-платформе значение сохраняется и потом подгружается автоматически

Текущая яркость

ui.setBrightness(35);
  • диапазон 0..100
  • сразу отправляет новое значение в текущий backlight runtime, то есть эффект виден без перезапуска GUI
  • если backlight настроен через setBacklight(...) или setBacklightHandler(...), библиотека сразу применяет новый уровень к активной подсветке
  • значение всегда clamp-ится сверху через maxBrightness(), поэтому setBrightness(90) при setMaxBrightness(70) даст фактические 70
ui.brightness();
  • возвращает именно текущее runtime-значение, а не сохранённый лимит
  • это временное пользовательское состояние на текущую сессию: после reboot оно не восстанавливается из prefs автоматически
  • если нужен сохраняемый предел яркости, для этого используется setMaxBrightness(...)

Низкоуровневые helpers

void myBacklight(uint16_t level) {
    // свой способ применить яркость
}

ui.setBacklightHandler(myBacklight);
  • setBacklightHandler(...) позволяет подставить свой handler для управления подсветкой
  • callback имеет сигнатуру void (*)(uint16_t level)
  • level это целевой уровень яркости, который библиотека хочет применить
  • это low-level хук для своего драйвера подсветки, если стандартного setBacklight() недостаточно
  • обычно этот API не нужен, если хватает setBacklight()

4. Логотип

ui.showLogo()
    .text("PISPPUS", "Digital Thermometer")
    .anim(FadeIn);

showLogo() запускает полноэкранную boot-анимацию с логотипом. Размер текста библиотека подбирает сама под текущее разрешение экрана.

Параметры:

  • text(title, subtitle) — две строки логотипа;
  • anim(...)None, FadeIn, LightFade;

Что делают анимации:

  • None - просто сразу показывает логотип без анимации
  • FadeIn - плавно проявляет текст из цвета фона
  • LightFade - плавно поднимает яркость подсветки; для неё backlight должен быть заранее настроен

5. Базовые helpers

5.1. Размеры

ui.screenWidth();   // ширина вашего экрана
ui.screenHeight();  // высота вашего экрана
  • screenWidth() и screenHeight() возвращают уже активный логический размер экрана
  • удобно для центровки, адаптивных отступов и расчёта layout без хардкода

5.2. Цвет

ui.rgb(255, 255, 255);
0xFFFF;     // RGB565
0xFF6200;   // RGB888
  • Библиотека поддерживает два практических способа задания цвета:
  • ui.rgb(r, g, b) - обычный и основной способ. На вход подаётся RGB888, на выходе получается RGB565.
  • прямой hex-литерал - если ты уже знаешь нужное значение заранее
  • если метод принимает uint16_t, туда нужно передавать RGB565
  • если метод принимает uint32_t, туда нужно передавать RGB888 в виде 0xRRGGBB

Для повторно используемых системных accent-цветов есть короткие semantic-токены:

Warning; // #FF6200
Error;   // #FF0048
  • их можно передавать в color(...), fillColor(...), bgColor(...) и другие места, где ожидается RGB565

5.3. Очистка экрана

ui.clear(ui.rgb(0, 0, 0));
  • очищает весь текущий draw target указанным цветом

5.4. Клип

ui.setClip()
    .pos(10, 20)
    .size(120, 80);

// ... рисование только внутри области ...

ui.clearClip();
  • setClip() ограничивает всю последующую отрисовку прямоугольной областью
  • удобно для локальных redraw, списков, анимаций и виджетов в карточках
  • clearClip() возвращает обычную отрисовку без ограничений

5.5. Наследование стиля fluent

У fluent-объектов есть derive(). Это позволяет собрать базовый стиль один раз, а потом сделать на его основе несколько вариантов без копирования всей цепочки.

Пример:

auto base = ui.drawButton()
    .size(120, 40)
    .baseColor(ui.rgb(0, 120, 255))
    .radius(10);

auto compact = base.derive()
    .size(96, 34);

base
    .label("Main")
    .pos(20, 160);

compact
    .label("Mini")
    .pos(20, 210);

После derive() исходный fluent становится шаблоном и сам больше не коммитится. Рисуется уже производная цепочка или финальная донастройка исходного объекта.

5.6. Adaptive preview

ui.setAdaptivePreview(240, 135, 7200);
ui.clearAdaptivePreview();
  • это debug-helper для проверки адаптивности интерфейса
  • setAdaptivePreview(minWidth, minHeight, cycleMs) плавно гоняет логический размер GUI между текущим физическим экраном и указанным минимумом
  • при этом screenWidth() и screenHeight() реально меняются, поэтому layout пересчитывается по-настоящему
  • это не замена configDisplay().size(...) и не перенастройка самой панели: физический дисплей остаётся тем же, меняется только логический viewport GUI
  • clearAdaptivePreview() выключает этот режим и возвращает обычный размер экрана

6. Layout helpers

Это layout-слой самого PipGUI: им удобно создавать контейнеры, делать gap и раскладывать компоненты без ручного сдвига каждого элемента.

Базовые типы:

UiSize   size{120, 40};
UiPoint  point{12, -8};
UiRect   rect{0, 0, 240, 320};
UiInsets insets{10, 10, 10, 10};
UiGap    gap{12, 8};
UiBox    box{ui.screenRect()};

6.1. Slicing API

UiRect root{0, 0, 240, 320};
UiRect work = inset(root, 10);

UiRect header = takeTop(work, 24, 8);
UiRect footer = takeBottom(work, 28, 8);
UiRect content = work;

UiRect, UiFlowRow, UiFlowColumn и UiBox это часть layout API PipGUI, а не просто внутренние структуры.

Если нужен именно контейнерный fluent-стиль, контейнер создаётся через ui.box() или ui.box(rect):

UiBox page = ui.box().inset(10);
UiRect header = page.top(24, 8);
UiRect body = page.rest();

UiBox panel = ui.box(ui.area(20, 60, 200, 120)).inset(8);
UiRect title = panel.top(20, 6);
UiRect content = panel.rest();

Как это читать:

  • ui.box() создаёт контейнер по всему экрану
  • ui.box(rect) создаёт контейнер внутри уже готовой области
  • inset(...) сужает контейнер изнутри
  • top / bottom / left / right / rest режут контейнер на части и возвращают UiRect
  • полученные UiRect потом передаются в .in(rect) у компонентов

Если нужен прямой low-level доступ, ui.screenRect() возвращает текущую экранную область как UiRect:

UiRect page = inset(ui.screenRect(), 10);
UiRect header = takeTop(page, 24, 8);
UiRect body = page;

Доступно:

  • inset(rect, all)
  • inset(rect, l, t, r, b)
  • inset(rect, UiInsets{...})
  • takeTop(...)
  • takeBottom(...)
  • takeLeft(...)
  • takeRight(...)
  • placeInside(...)
  • centerIn(...)

6.2. Flow API

UiSize sizes[3] = {{40, 20}, {60, 20}, {40, 20}};
UiRect out[3];

flowRow(area, sizes, out, 3, 10, Center, Center);
flowColumn(area, sizes, out, 3, 8, Center, Center);

Для распределения доступны:

  • Start
  • Center
  • End
  • layout::SpaceBetween
  • layout::SpaceEvenly

Любой UiRect можно использовать как лёгкий контейнер: у drawText(), drawRect(), drawCircle(), drawEllipse(), drawArc(), drawTriangle(), drawLine() и drawSquircleRect() есть .in(rect), после чего pos(...) и локальные координаты считаются уже внутри этой области.

В pos(...) для координаты center можно быстро поставить объект по центру соответствующей оси. Остальные значения остаются обычными координатами.

Практический паттерн:

UiBox page = ui.box().inset(10);
UiRect row = page.top(40, 8);

UiFlowRow<3> flow(row, 10, Start, Center);
UiRect &a = flow.next(40, 20);
UiRect &b = flow.next(60, 20);
UiRect &c = flow.next(40, 20);
flow.finish();

ui.drawRect().in(a).size(40, 20).fill(ui.rgb(0, 120, 255));
ui.drawText().in(b).pos(center, center).text("Hello").align(Center);
ui.drawTriangle().in(c).pos(center, center).size(20, 14).direction(Up);

6.3. Cursor-based API

UiFlowRow<3> row(area, 10, layout::SpaceEvenly, Center);

row.next(40, 24);
row.next(60, 24);
row.next(40, 24);
row.finish();

Аналогично работает UiFlowColumn<N>.

Как этим пользоваться правильно:

  • next(w, h) резервирует следующий slot
  • после всех next(...) нужно вызвать finish()
  • итоговые прямоугольники потом берутся через row[i] или column[i]

Для позиционирования и выравнивания часто используются короткие шорткаты:

  • center для автоматического центрирования по оси
  • Left, Center, Right
  • Top, Bottom

7. Текст, шрифты и иконки

7.1. Встроенные шрифты

Текущий шрифт настраивается отдельными методами:

ui.setFont(WixMadeForDisplay);   // выбирает семейство шрифта
ui.setFontSize(18);              // задаёт текущий размер в пикселях

В библиотеке сейчас есть два встроенных семейства:

  • WixMadeForDisplay
  • KronaOne

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

ui.setFontWeight(450);        // обычно это диапазон `100..900`
ui.setFontWeight(Semibold);
  • Thin
  • Light
  • Regular
  • Medium
  • Semibold
  • Bold
  • Black

Текущее состояние тоже можно читать:

ui.fontId();      // текущий FontId
ui.fontSize();    // текущий размер шрифта
ui.fontWeight();  // текущая толщина шрифта

7.2. Текстовые стили

ui.setTextStyle(H1);

setTextStyle(...) быстро выставляет готовый пресет под тип текста.

Доступные стили:

  • H1 - крупный заголовок
  • H2 - подзаголовок или вторичный заголовок
  • Body - основной текст интерфейса
  • Caption - мелкие подписи и пояснения

7.3. Обычный текст

ui.drawText()
    .pos(center, 32)                   // `center` работает и по X, и по Y
    .font(WixMadeForDisplay, 18)       // конкретный шрифт, размер и насыщенность
    .weight(Semibold)                  // вес текста
    .text("Hello")                     // сама строка
    .color(ui.rgb(255, 255, 255))      // цвет текста
    .bgColor(ui.rgb(0, 0, 0))          // фон под текстом
    .align(Center);                    // горизонтальный якорь текста

drawText() рисует строку сразу в текущий кадр.

Если текст обновляется на одном и том же месте, используйте updateText():

ui.updateText()
    .pos(center, 32)
    .text("Updated")
    .color(ui.rgb(255, 255, 255))
    .align(Center);
  • drawText() рисует текст как есть
  • updateText() сначала очищает прошлую область и потом рисует новое значение на том же месте
  • align(...) отвечает только за горизонтальный якорь строки, когда x задан числом
  • in(rect) делает rect локальным контейнером для текста
  • pos(center, center) центрирует строку как объект по обеим осям

7.4. Текстовый блок с переносом слов

ui.drawTextBox()
    .pos(24, 128)                               // левый верхний угол области
    .size(192, 84)                              // ширина и высота области
    .text("This box wraps words automatically and clips everything outside.")
    .color(ui.rgb(220, 240, 240))
    .bgColor(ui.rgb(0, 46, 46))
    .align(Left);                               // выравнивание каждой строки внутри области

drawTextBox():

  • переносит слова автоматически по ширине области
  • рисует текст только внутри заданного прямоугольника
  • поддерживает .in(rect), если область уже получена из layout
  • по умолчанию сам ставит нормальный межстрочный интервал
  • если нужен свой интервал, его можно переопределить через .lineGap(...)
  • .align(Left / Center / Right) задаёт выравнивание строк внутри области

7.5. Бегущая строка и многоточие

ui.drawTextMarquee()
    .pos(20, 80)                                  // точка привязки строки
    .width(140)                                   // ширина области, внутри которой идёт прокрутка
    .font(WixMadeForDisplay, 18)                  // конкретный шрифт и его размер
    .weight(Semibold)                             // насыщенность начертания
    .text("Very long text that does not fit")     // сама строка
    .color(ui.rgb(255, 255, 255))                 // цвет текста
    .speed(30)                                    // скорость движения в пикселях в секунду
    .holdStart(700)                               // пауза перед началом движения
    .phaseStart(0)                                // стартовая фаза, если нужна синхронизация
    .align(Left);                                 // выравнивание строки внутри заданной ширины

drawTextMarquee() нужен для длинной строки, которая должна прокручиваться внутри ограниченной ширины.

ui.drawTextEllipsized()
    .pos(20, 110)                                 // точка привязки строки
    .width(140)                                   // ширина области, в которую текст должен уместиться
    .font(WixMadeForDisplay, 18)                  // конкретный шрифт и его размер
    .weight(Semibold)                             // насыщенность начертания
    .text("Very long text that does not fit")     // сама строка
    .color(ui.rgb(255, 255, 255));                // цвет текста

drawTextEllipsized() обрезает строку по width(...) и добавляет многоточие

7.6. Иконки

Обычные иконки берутся из набора IconId

ui.drawIcon()
    .pos(20, 20)                      // позиция иконки
    .size(18)                         // итоговый размер в пикселях
    .icon(warning)                    // конкретный `IconId`
    .color(ui.rgb(255, 255, 255))     // основной цвет слоя
    .bgColor(ui.rgb(0, 0, 0));        // фон под иконкой

Отдельного updateIcon() нет.

Если иконка многослойная, слои можно рисовать отдельно разными цветами:

ui.drawIcon()
    .pos(200, 10)
    .size(18)
    .icon(battery_l0)
    .color(ui.rgb(255, 255, 255));

ui.drawIcon()
    .pos(200, 10)
    .size(18)
    .icon(battery_l1)
    .color(ui.rgb(0, 200, 120));

Для animated icons используются отдельные runtime-вызовы.

Обычная отрисовка:

ui.drawAnimIcon()
    .pos(92, 120)
    .size(56)
    .icon(setting_anim)
    .color(ui.rgb(235, 235, 235));

drawAnimIcon() рисует анимированную иконку как есть

Локальное обновление поверх известного фона:

ui.updateAnimIcon()
    .pos(92, 120)
    .size(56)
    .icon(setting_anim)
    .color(ui.rgb(235, 235, 235))
    .bgColor(ui.rgb(10, 10, 10));

updateAnimIcon() сначала очищает область через bgColor, потом рисует новый кадр

Создание иконок из sources

Чтобы добавить свою иконку:

  1. положить source-файл в Tools/Icons/Sources/
  2. пересобрать проект — генератор сам обновит готовые файлы в lib/PipKit/PipGUI/Graphics/Text/Icons/
  3. использовать иконку по имени файла

Пример:

  • файл Tools/Icons/Sources/checkmark.svg
  • в коде: .icon(checkmark)

Что важно:

  • генератор читает и .svg, и .json рекурсивно
  • файлы могут лежать как прямо в корне sources, так и в любых подпапках
  • имя для C++ генерируется из относительного пути, так что вложенные папки тоже поддерживаются

Что появится в коде:

  • для обычной однослойной иконки из name.svg появляется alias name
  • для многослойной иконки появляются alias вида name_l0, name_l1, name_l2
  • дополнительно экспортируются и enum-константы IconName, IconNameL0, IconNameL1 и т.д.
  • для анимированной иконки из name.json появляется alias name_anim

Если в имени файла есть -, пробелы или другие неподходящие символы, генератор сам приводит имя к валидному C++-идентификатору через _.

  • .svg идёт в обычный static PSDF pipeline.
  • .json идёт в animated PSDF pipeline.

8. Фигуры

Что важно:

  • fill(color565) — задаёт заливку; если не вызывать, фигура остаётся без заливки
  • border(widthPx, color565) — задаёт контур; если не вызывать, контура не будет
  • fill(...) и border(...) можно использовать вместе или по отдельности

8.1 Линия

ui.drawLine()
    .from(20, 20)                      // начало линии
    .to(140, 60)                       // конец линии
    .thickness(2)                      // толщина линии; по умолчанию `1`
    .color(ui.rgb(255, 255, 255))      // цвет линии

8.2 Прямоугольник

ui.drawRect()
    .pos(20, 40)                          // левый верхний угол прямоугольника
    .size(100, 40)                        // ширина и высота
    .radius(10)                           // один радиус для всех углов
    // .radius(10, 14, 18, 22)            // или отдельные радиусы: tl, tr, br, bl
    .fill(ui.rgb(0, 120, 255))            // цвет заливки
    .border(1, ui.rgb(255, 255, 255))     // толщина и цвет контура

8.3 Круг

ui.drawCircle()
    .pos(50, 50)                          // центр круга
    .radius(18)                           // радиус
    .fill(ui.rgb(0, 87, 250))             // цвет заливки
    .border(1, ui.rgb(255, 255, 255))     // толщина и цвет контура

8.4 Эллипс

ui.drawEllipse()
    .pos(120, 50)                         // центр эллипса
    .radiusX(28)                          // горизонтальная полуось
    .radiusY(16)                          // вертикальная полуось
    .fill(ui.rgb(255, 0, 72))             // цвет заливки
    .border(1, ui.rgb(255, 255, 255))     // толщина и цвет контура

8.5 Треугольник

ui.drawTriangle()
    .pos(70, 100)                          // позиция фигуры
    .size(60, 40)                          // готовый isosceles preset
    .direction(Up)                         // Up / Right / Down / Left
    .radius(8)                             // опционально: скругление углов
    .fill(ui.rgb(0, 200, 120))             // цвет заливки
    .border(1, ui.rgb(255, 255, 255))      // толщина и цвет контура

Для произвольной формы точки задаются локально, а pos(...) только переносит готовую фигуру:

ui.drawTriangle()
    .pos(140, 140)
    .vertices(-30, 12, 30, 12, 0, -26)
    .fill(ui.rgb(255, 128, 0))
    .border(1, ui.rgb(255, 255, 255))

Что значат числа в .vertices(...):

  • формат: .vertices(x0, y0, x1, y1, x2, y2)
  • это 3 локальные вершины относительно pos(...)
  • 0, 0 это локальный origin фигуры
  • отрицательные значения уходят влево/вверх, положительные вправо/вниз

Пример:

ui.drawTriangle()
    .pos(140, 140)
    .vertices(-30, 12, 30, 12, 0, -26);   // основание внизу, вершина сверху

8.6 Дуга

ui.drawArc()
    .pos(100, 80)                     // центр дуги
    .radius(28)                       // внешний радиус
    .thickness(6)                     // толщина; по умолчанию 1
    .start(-90.0f)                    // начальный угол в градусах
    .end(90.0f)                       // конечный угол в градусах
    .color(ui.rgb(80, 255, 120))      // цвет дуги

У дуги концы всегда скруглённые, а диапазон 0..360 рисует полный круг.

8.7 Сквиркль

ui.drawSquircleRect()
    .pos(54, 134)                         // левый верхний угол области
    .size(52, 52)                         // ширина и высота
    .radius(26)                           // один радиус для всех углов
    // .radius(18, 22, 26, 30)            // или отдельные радиусы: tl, tr, br, bl
    .fill(ui.rgb(255, 128, 0))            // цвет заливки
    .border(1, ui.rgb(255, 255, 255))     // толщина и цвет контура

9. Градиенты

У всех градиентов pos(...) и size(...) задают прямоугольную область рисования.

9.1. Вертикальный

ui.gradientVertical()
    .pos(20, 20)
    .size(120, 40)
    .TColor(ui.rgb(255, 0, 72))
    .BColor(ui.rgb(0, 87, 250))
  • цвет плавно меняется сверху вниз

9.2. Горизонтальный

ui.gradientHorizontal()
    .pos(20, 70)
    .size(120, 40)
    .LColor(ui.rgb(255, 128, 0))
    .RColor(ui.rgb(80, 255, 120))
  • цвет плавно меняется слева направо

9.3. Диагональный

ui.gradientDiagonal()
    .pos(20, 170)
    .size(120, 40)
    .TLColor(ui.rgb(255, 255, 255))
    .BRColor(ui.rgb(30, 30, 30))
  • плавный переход по диагонали от верхнего левого к нижнему правому углу

9.4. 4 угла

ui.gradientCorners()
    .pos(20, 120)
    .size(120, 40)
    .TLColor(ui.rgb(255, 0, 72))
    .TRColor(ui.rgb(0, 87, 250))
    .BLColor(ui.rgb(80, 255, 120))
    .BRColor(ui.rgb(255, 128, 0))
  • каждая вершина прямоугольника получает свой цвет, внутри идёт двунаправленная интерполяция между четырьмя углами

9.5. Радиальный

ui.gradientRadial()
    .pos(20, 220)
    .size(120, 60)
    .center(80, 250)
    .radius(40)
    .innerColor(ui.rgb(255, 255, 255))
    .outerColor(ui.rgb(0, 87, 250))
  • center(...) задаёт центр градиента в координатах экрана
  • radius(...) задаёт радиус перехода
  • innerColor(...) и outerColor(...) задают цвета в центре и на периферии

10. Эффекты

10.1. Blur

ui.drawBlur()
    .pos(0, 180)                  // левый верхний угол blur-области
    .size(240, 40)                // ширина и высота области
    .radius(10)                   // сила blur; чем больше, тем мягче и тяжелее эффект
    .direction(TopDown)           // если указать, material пойдёт по этому направлению
    .material(160, -1)            // сила material и его цвет; -1 = цвет фона библиотеки

Для обновления части экрана:

ui.updateBlur()
    .pos(0, 180)                  // та же область, которую нужно перерисовать
    .size(240, 40)                // тот же размер blur-региона
    .radius(10)                   // тот же blur-радиус
    .direction(TopDown)           // то же направление материала
    .material(160, -1)            // те же параметры material

updateBlur() нужен для in-place перерисовки той же blur-области без полной перерисовки экрана.

direction(...) поддерживает:

  • TopDown - материал и его усиление идут сверху вниз
  • BottomUp - материал и его усиление идут снизу вверх
  • LeftRight - материал и его усиление идут слева направо
  • RightLeft - материал и его усиление идут справа налево

material(strength, color) управляет только верхним tinted-слоем поверх blur:

  • чем больше strength, тем заметнее tinted-слой поверх blur
  • color = -1 - взять текущий цвет фона библиотеки

То есть blur остаётся тем же, а меняется только то, как поверх него распределяется tinted-слой.

10.2. Glow

Круг:

ui.drawGlowCircle()
    .pos(60, 90)                          // центр круга
    .radius(18)                           // радиус фигуры
    .fillColor(ui.rgb(255, 255, 255))     // цвет самой фигуры
    .glowColor(ui.rgb(0, 120, 255))       // цвет свечения вокруг
    .glowSize(16)                         // толщина зоны свечения
    .glowStrength(220)                    // интенсивность свечения
    .anim(Pulse)                          // тип анимации свечения
    .pulseMs(1200)                        // период пульсации в миллисекундах

Для in-place обновления есть updateGlowCircle(). Если glow нужно обновлять на месте без грязных хвостов, добавь bgColor(...) с цветом фона под фигурой.

anim(...) поддерживает:

  • None - свечение статичное, без анимации
  • Pulse - свечение плавно пульсирует по силе

11. Экраны и цикл

11.1. Регистрация экранов

Экраны регистрируются через макрос SCREEN(name, order):

SCREEN(ScreenHome, 0)
{
    ui.clear(ui.rgb(0, 0, 0));
}

SCREEN(ScreenSettings, 1)
{
    ui.clear(ui.rgb(8, 8, 8));
}

Что создаёт макрос:

  • callback-функцию экрана;
  • числовой id экрана;
  • автоматическую регистрацию экрана в таблице GUI.

Что важно:

  • name становится константой id, её потом можно передавать в setScreen(...), updateList(...), updateTile(...) и другие API
  • order задаёт порядок регистрации экрана
  • order должен быть уникальным для каждого экрана
  • обычно первый экран делают с order = 0

После этого экран можно активировать:

ui.setScreen(ScreenHome);

11.2. Основной цикл

Базовый вариант:

void loop()
{
    ui.loop();
}

Вспомогательный вариант, если есть две кнопки Button:

Button Next(1, Pullup);
Button Prev(2, Pullup);

void setup()
{
    Next.begin();
    Prev.begin();
}

void loop()
{
    ui.loopWithInput(Next, Prev);
}

Вариант с тремя кнопками (Next/Prev/Select):

Button Next(1, Pullup);
Button Prev(2, Pullup);
Button Select(3, Pullup);

void setup()
{
    Next.begin();
    Prev.begin();
    Select.begin();
}

void loop()
{
    ui.loopWithInput(Next, Prev, Select);
}

loopWithInput(...) обновляет объекты Button, собирает InputState, диспатчит события в built-in overlay/navigation handlers и затем вызывает ui.loop(). loopWithPolledInput() делает то же самое, но использует последний pollInput(...).

Экран сам явно выбирает, как трактовать кнопки:

(void)ui.listNav();            // привязать built-in list-controller к текущему экрану
(void)ui.tileNav();            // привязать built-in tile-controller к текущему экрану
ui.nav().handler(myHandler);   // привязать свой event-handler
ui.nav().clear();              // снять handler с текущего экрана

Сигнатура custom handler:

bool myHandler(GUI &ui, const NavEvent &event, void *userData)
{
    return false;
}
  • вернуть true значит "событие обработано"
  • вернуть false значит "этот handler событие не использовал"
  • userData передаётся через ui.nav().handler(myHandler, userData)

Если loopWithInput(...) не используется, вызывайте btn.update() сами в начале каждого loop(). После этого wasPressed() и isDown() читают уже обновлённое состояние кнопки.

Если нужен готовый snapshot ввода для собственного state-machine:

InputState input = ui.pollInput(Next, Prev);

или (если есть кнопка выбора):

InputState input = ui.pollInput(Next, Prev, Select);

pollInput(...) сам обновляет кнопки и возвращает готовый снимок их состояния на текущий тик.

Поля InputState:

  • nextDown - кнопка Next сейчас удерживается
  • prevDown - кнопка Prev сейчас удерживается
  • nextPressed - кнопка Next была нажата именно в этом тике
  • prevPressed - кнопка Prev была нажата именно в этом тике
  • selectDown - кнопка Select сейчас удерживается (если включен 3-button режим)
  • selectPressed - кнопка Select была нажата именно в этом тике (если включен 3-button режим)
  • comboDown - обе кнопки сейчас зажаты одновременно
  • hasSelect - true, если pollInput/loopWithInput вызваны в 3-button режиме

Разница простая:

  • Down - состояние удержания
  • Pressed - одноразовое событие нажатия

10.2.1. NavEvent и готовые handlers

NavEvent содержит уже нормализованное событие навигации:

event.screenId;     // экран, для которого идёт dispatch
event.button;       // Next / Prev / Select / Combo
event.code;         // Pressed / LongPressed / Repeat / Released
event.nowMs;        // timestamp события
event.heldMs;       // сколько удерживалась кнопка
event.longPress;    // был ли уже long-press для этого release
event.nextDown;
event.prevDown;
event.selectDown;
event.comboDown;
event.hasSelect;

Коды событий:

  • Pressed - первое нажатие кнопки
  • LongPressed - момент, когда удержание перешло порог long-press
  • Repeat - повторные тики после long-press
  • Released - отпускание кнопки

Встроенные handlers:

  • ui.listNav():
    • Next Released - следующий пункт
    • Prev Released - предыдущий пункт
    • Prev LongPressed - backScreen()
    • Select Released в 3-button режиме - открыть targetScreen
    • Next LongPressed в 2-button режиме - открыть targetScreen
    • Combo Released - открыть targetScreen
  • ui.tileNav():
    • та же семантика, что и у listNav(), но для tile-menu

Что библиотека продолжает обрабатывать вне screen-nav handler:

  • Скриншоты (built-in shortcut): удержание Next + Prev при включённых screenshots
  • Popup menu: использует свой built-in handler поверх того же input pipeline
  • Notification overlay: использует свой built-in handler
  • Error overlay: использует свой built-in handler
  • Slider / graph-specific helpers: читают последний InputState, если сам виджет это делает

11.3. Управление экранами

Эти методы управляют активным экраном и переходами между экранами.

ui.setScreen(ScreenHome);      // сразу переключает на указанный экран
ui.currentScreen();            // возвращает id текущего активного экрана
ui.nextScreen();               // переходит на следующий экран по порядку регистрации
ui.prevScreen();               // переходит на предыдущий экран по порядку регистрации (циклически)
ui.backScreen();               // возвращает назад по navigation-history
ui.screenTransitionActive();   // показывает, что переход между экранами сейчас ещё идёт

Что важно:

  • библиотека сама ведёт history переходов между экранами (для backScreen())
  • если экран меняется через setScreen(...), текущий экран добавляется в history автоматически

11.4. Принудительная перерисовка

ui.requestRedraw();

Нужно, когда данные экрана изменились вне обычного render-flow, и вы хотите гарантированно перерисовать следующий кадр.

11.5. Анимация переходов

ui.setScreenAnim(SlideX, 250);

setScreenAnim(mode, durationMs) задаёт стиль и длительность перехода между экранами.

Доступные режимы:

  • None - переход без анимации
  • SlideX - горизонтальный слайд
  • SlideY - вертикальный слайд

12. Статус-бар

12.1. Build-time флаги

  • PIPGUI_STATUS_BAR
    • 1 включает код статус-бара
    • 0 вырезает runtime-реализацию, публичные методы остаются no-op
  • PIPGUI_DEBUG_METRICS
    • 1 включает debug-режим: библиотека рисует диагностический текст в статус-баре (FPS/время кадра/память и т.п.)
    • 0 выключено (по умолчанию)

Обычно эти флаги задаются в include/config.hpp.

12.2. Включение

ui.configStatusBar()
    .height(18)
    .pos(Top)
    .style(Blur);

Позиции:

  • Top
  • Bottom

Стили:

  • Solid — обычная непрозрачная полоса; layout резервирует под неё высоту
  • Blur — блюр-полоса поверх контента; layout не должен откусывать под неё safe area

12.3. Текст

ui.setStatusBarText()
    .left("PipGUI")
    .center("12:34")
    .right("Wi-Fi");

12.4. Батарея

ui.setStatusBarBattery(87, Numeric);
ui.setStatusBarBattery(100, Bar);
ui.setStatusBarBattery(-1, Hidden);

BatteryStyle:

  • Hidden
  • Numeric
  • Bar
  • WarningBar
  • ErrorBar

12.5. Кастомная дорисовка

void statusBarCustom(GUI &ui, int16_t x, int16_t y, int16_t w, int16_t h)
{
    ui.drawLine()
        .from(x, y + h - 1)
        .to(x + w, y + h - 1)
        .color(ui.rgb(70, 70, 70))
        .draw();
}

ui.setStatusBarCustom(statusBarCustom);

Вспомогательные методы:

int16_t h = ui.statusBarHeight();
ui.updateStatusBar();
ui.renderStatusBar();

Что важно:

  • statusBarHeight() возвращает ненулевую высоту только для Solid
  • при Blur helper возвращает 0, потому что layout не должен резервировать fixed-height safe area под blur-панель

12.6. Иконки слотов

Можно повесить отдельную иконку в левый, центральный или правый слот статус-бара:

ui.setStatusBarIcon()
    .side(Left)
    .icon(warning);

ui.setStatusBarIcon()
    .side(Center)
    .icon(error)
    .color(ui.rgb(255, 80, 80))
    .size(16);

ui.setStatusBarIcon()
    .side(Right)
    .icon(warning)
    .color(ui.rgb(255, 220, 120))
    .size(14);

Параметры:

  • первый аргумент - слот: Left, Center или Right
  • второй аргумент - IconId
  • color необязателен; если не задан, берётся foreground-цвет статус-бара
  • sizePx необязателен; если 0, размер подбирается автоматически от высоты панели

Удаление иконки:

ui.clearStatusBarIcon(Left);
ui.clearStatusBarIcon(Center);
ui.clearStatusBarIcon(Right);

Поведение:

  • иконки появляются и исчезают с короткой fade-анимацией
  • левая иконка живёт в одном block-е с левым текстом
  • центральная иконка центрируется вместе с центральным текстом как одна группа
  • правая иконка живёт в правом block-е вместе с правым текстом

13. Виджеты

13.1. Scroll dots

ui.drawScrollDots()
    .pos(center, 220)
    .count(5)
    .activeIndex(2)
    .activeColor(ui.rgb(0, 87, 250))
    .inactiveColor(ui.rgb(60, 60, 60)) 
    .radius(3)
    .spacing(14);

Есть и updateScrollDots() с теми же параметрами.

Что задают параметры:

  • count(...) - общее число страниц/точек
  • activeIndex(...) - текущая активная страница
  • radius(...) - базовый радиус точки
  • spacing(...) - шаг между центрами соседних точек

Поведение:

  • при count > 7 включается оконный режим: показывается компактное окно точек с taper по краям

13.2. Buttons

Обычная отрисовка:

ui.drawButton()
    .label("Save")                       // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(120, 40)                       // ширина и высота кнопки
    .baseColor(ui.rgb(0, 120, 255))      // основной цвет кнопки
    .radius(10);                         // радиус

Обновление с состоянием кнопки:

ui.updateButton()
    .label("Save")                       // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(120, 40)                       // ширина и высота кнопки
    .baseColor(ui.rgb(0, 120, 255))      // основной цвет кнопки
    .radius(10)                          // радиус
    .icon(warning)                       // иконка внутри кнопки
    .mode(true, false)                   // enabled, loading
    .down(isDown)                        // текущее физическое нажатие для press-анимации
;

Кнопка с прогрессом:

ui.updateButton()
    .label("Updating 56%")               // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(170, 38)                       // ширина и высота кнопки
    .baseColor(ui.rgb(24, 24, 24))       // базовый цвет корпуса кнопки
    .fillColor(ui.rgb(0, 120, 255))      // цвет progress-заливки
    .value(56)                           // значение встроенного progress: 0..100
    .radius(12)                          // радиус
    .icon(battery_l1);                   // иконка внутри кнопки

drawButton() и updateButton() используют один и тот же API. Для обычных статичных экранов достаточно drawButton(). Для анимируемой или интерактивной кнопки используй updateButton().

Если заданы и текст, и иконка - иконка рисуется слева от текста как единый центрированный блок. Если текст пустой, иконка рисуется по центру кнопки. Если задан value(...), кнопка рисует встроенный progress-fill под текстом и иконкой. loading и progress одновременно не используются: progress-режим приоритетнее.

13.3. Toggle switch

Снаружи нужен только обычный bool:

bool wifiEnabled = false;
bool changed = false;

Отрисовка:

ui.updateToggleSwitch()
    .pos(center, 140)
    .size(78, 36)
    .value(wifiEnabled)                   // текущий bool; библиотека сама обновит его при нажатии
    .pressed(btn.wasPressed())            // событие нажатия этого кадра
    .changed(changed)                     // сюда вернется true, если значение переключилось
    .enabled(!wifiBusy)                   // можно временно отключить ввод, пока идет внешняя операция
    .activeColor(ui.rgb(21, 180, 110))    // цвет включенного track
    .inactiveColor(ui.rgb(46, 46, 46))    // цвет выключенного track
    .knobColor(0xFFFF);                   // цвет бегунка

drawToggleSwitch() и updateToggleSwitch() используют один и тот же fluent API. Разница только в режиме вывода:

  • drawToggleSwitch() - обычная отрисовка
  • updateToggleSwitch() - локальный dirty-update

Если хочется просто показать состояние без ввода, можно не передавать pressed(...) и changed(...):

ui.drawToggleSwitch()
    .pos(center, 140)
    .size(78, 36)
    .value(wifiEnabled)
    .activeColor(ui.rgb(21, 180, 110));

13.4. Slider

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

ui.updateSlider()
    .pos(center, 114)                     // центр по X, координата Y
    .size(186, 24)                        // ширина и высота трека
    .bind(value)                          // привязка переменной; библиотека обновляет ее сама
    .activeColor(ui.rgb(0, 87, 250))      // цвет заполненной части
    .inactiveColor(ui.rgb(36, 36, 36))    // цвет неактивной части трека
    // .enabled(false)                    // опционально: показать slider без реакции на ввод
    .thumbColor(0xFFFF);                  // цвет бегунка

drawSlider() и updateSlider() используют один и тот же fluent API. Для интерактивного сценария нужен updateSlider(). Ввод Next/Prev slider берет сам из последнего pollInput(...).

Поведение:

  • удержание кнопки сначала двигает значение обычным шагом, потом ускоряет и частоту, и величину шага;
  • по умолчанию бегунок белый, трек темнее активной части.

13.5. Прогресс

ui.drawProgress()
    .pos(20, 220)                        // левый верхний угол
    .size(180, 16)                       // ширина и высота
    .value(65)                           // значение progress: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .radius(8)                           // скругление краев
    .anim(Shimmer);                      // тип анимации progress

Для локального обновления без полной перерисовки есть updateProgress():

ui.updateProgress()
    .pos(20, 220)                        // та же область, которую нужно обновить
    .size(180, 16)                       // тот же размер
    .value(65)                           // новое значение: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .radius(8)                           // скругление краев
    .anim(Shimmer);                      // тип анимации progress

Текст у линейного прогресса задаётся прямо на самом progress:

ui.drawProgress()
    .pos(20, 246)                            // левый верхний угол
    .size(180, 14)                           // ширина и высота
    .value(65)                               // значение progress: 0..100
    .baseColor(ui.rgb(20, 20, 20))           // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))          // цвет заполненной части
    .label("Downloading", Left)              // текст и его выравнивание
    .labelColor(ui.rgb(255, 255, 255))       // цвет label
    .percent(Right)                          // показать текущее значение как процент и задать выравнивание
    .percentColor(ui.rgb(200, 200, 200));    // цвет процента

Текст поддерживается только у линейного progress; у drawCircleProgress() его нет.

Для label(...) и percent(...) доступны:

  • Left - прижать текст к левому краю progress
  • Center - выровнять текст по центру progress
  • Right - прижать текст к правому краю progress

anim(...) у progress поддерживает:

  • None - progress статичный, без анимации
  • Shimmer - по заполненной части идет мягкий движущийся блик

13.6. Круговой прогресс

ui.drawCircleProgress()
    .pos(center, 140)                    // центр кольца
    .radius(34)                          // внешний радиус
    .thickness(8)                        // толщина кольца
    .value(72)                           // значение progress: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .anim(None);                         // тип анимации progress

Локальное обновление:

ui.updateCircleProgress()
    .pos(center, 140)                    // тот же центр
    .radius(34)                          // тот же внешний радиус
    .thickness(8)                        // та же толщина кольца
    .value(72)                           // новое значение: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .anim(None);                         // тип анимации progress

13.7. Drum roll

Горизонтальный:

ui.drawDrumRoll()
    .pos(20, 60)                            // левый верхний угол
    .size(200, 40)                          // ширина и высота области
    .options(16, "Low", "Medium", "High")   // шрифт и список опций; количество считается автоматически
    .selected(1)                            // текущий выбранный индекс
    .fgColor(ui.rgb(255, 255, 255))         // цвет текста
    .bgColor(ui.rgb(0, 0, 0));              // цвет фона

Вертикальный:

ui.drawDrumRoll()
    .pos(220, 100)                          // левый верхний угол
    .size(70, 90)                           // ширина и высота области
    .options(16, "Low", "Medium", "High")   // шрифт и список опций; количество считается автоматически
    .selected(1)                            // текущий выбранный индекс
    .fgColor(ui.rgb(255, 255, 255))         // цвет текста
    .bgColor(ui.rgb(0, 0, 0))               // цвет фона
    .vertical();                            // включить вертикальный режим

14. Списки и плитки

14.1. Списочное меню

Прямо в SCREEN(...):

SCREEN(ScreenMainMenu, 1)
{
    ui.clear(0x0000);

    ui.updateList()
    .items(
        listItem("Settings", "Device configuration", ScreenSettings),
        listItem("About",    "Firmware info",        ScreenAbout),
        listItem("Restart",  "Reboot device",        ScreenRestart))
        .inactive(ui.rgb(12, 12, 12))      // цвет обычной карточки
        .active(ui.rgb(0, 120, 255))       // цвет активной карточки
        // .checked(1)                        // опционально: справа рисовать галочку у этого пункта
        .radius(8)                         // радиус карточек
        .cardSize(0, 0)                    // 0,0 = размер подберётся автоматически
        .mode(Cards);                      // режим списка
}

Режимы списка:

  • Cards
  • Plain

Поведение:

  • короткое отпускание NEXT переключает пункт вперёд;
  • короткое отпускание PREV переключает пункт назад;
  • 2-button режим: удержание NEXT открывает targetScreen выбранного пункта;
  • 3-button режим: короткое нажатие SELECT открывает targetScreen выбранного пункта;
  • удержание PREV возвращает на предыдущий экран из navigation-history.

14.2. Плиточное меню

Прямо в SCREEN(...):

SCREEN(ScreenTiles, 2)
{
    ui.updateTile()
    .items(
        tileItem("Main",     "Главный экран", ScreenHome),
        tileItem("Settings", "Настройки",     ScreenSettings),
        tileItem("Info",     "Инфо",          ScreenInfo),
        tileItem("Graph",    "Графики",       ScreenGraph))
        .inactive(ui.rgb(16, 16, 16))     // обычная плитка
        .active(ui.rgb(0, 120, 255))      // активная плитка
        .radius(12)                       // радиус плитки
        .spacing(10)                      // расстояние между плитками
        .columns(2)                       // количество колонок в обычной сетке
        .tileSize(100, 70)                // желаемый размер плитки
        .mode(TextSubtitle);              // контент плитки
}

Поведение:

  • короткое отпускание NEXT переключает плитку вперёд;
  • короткое отпускание PREV переключает плитку назад;
  • 2-button режим: удержание NEXT открывает targetScreen выбранной плитки;
  • 3-button режим: короткое нажатие SELECT открывает targetScreen выбранной плитки;
  • удержание PREV возвращает на предыдущий экран из navigation-history.

Режимы плитки:

  • TextOnly
  • TextSubtitle

14.3. Кастомная раскладка плиток

Если обычной сетки мало, можно задать свою понятную раскладку:

ui.updateTile()
    .grid(2, 3)                                      // сетка: 2 колонки, 3 строки
    .tile("Main", "Главный экран", ScreenHome)
    .at(0, 0)                                        // колонка 0, строка 0
    .span(2, 1)                                      // ширина 2 клетки, высота 1 клетка
    .tile("Settings", "Настройки", ScreenSettings)
    .at(0, 1)                                        // колонка 0, строка 1
    .tile("Info", "Инфо", ScreenInfo)
    .at(1, 1)                                        // колонка 1, строка 1
    .tile("Graph", "Графики", ScreenGraph)
    .at(1, 2)                                        // колонка 1, строка 2

Что важно:

  • grid(cols, rows) включает кастомную сетку
  • tile(...) добавляет очередную плитку
  • at(col, row) ставит последнюю добавленную плитку в нужную клетку
  • span(cols, rows) растягивает последнюю добавленную плитку на несколько клеток
  • в этом режиме columns(...) уже не влияет на раскладку

15. Графики

15.1. Фон и сетка:

ui.drawGraphGrid()
    .pos(10, 50)
    .size(220, 120)
    .radius(8)
    .direction(LeftToRight)           // задаёт режим движения и раскладки данных внутри этой graph-area
    .bgColor(ui.rgb(10, 10, 10))      // цвет сетки вычисляется автоматически из bgColor()
    .speed(1.0f);
  • drawGraphGrid() / updateGraphGrid() должны использовать тот же direction(...), в котором потом рисуются линии этого графа

Направления:

  • LeftToRight - новые точки приходят справа, старая история уезжает влево
  • RightToLeft - новые точки приходят слева, старая история уезжает вправо
  • Oscilloscope - фиксированное окно по всей ширине графика без rolling-shift; точки раскладываются по видимому буферу как осциллограф

Для локального dirty-redraw доступен тот же fluent через updateGraphGrid():

ui.updateGraphGrid()
    .pos(10, 50)
    .size(220, 120)
    .radius(8)
    .direction(LeftToRight)
    .bgColor(ui.rgb(10, 10, 10))
    .speed(1.0f);

15.2. Линия графика:

ui.drawGraphLine()
    .line(0)
    .value(sensorValue)
    .thickness(2)                   // задаёт толщину линии графика; по умолчанию `1`
    .color(ui.rgb(0, 255, 140))
    .range(0, 100);
    .scale(true);                   // вкл/выкл автоматический диапазон по данным для этой graph-area
  • drawGraphLine() добавляет новую точку в уже настроенный график
  • updateGraphLine() подходит для in-place обновления, когда графику нужно самому зачистить и перерисовать нужную область

15.3. Пакетная отрисовка готового массива:

int16_t samples[] = {10, 15, 12, 18, 20, 17};

ui.drawGraphSamples()
    .line(0)
    .samples(samples, 6)
    .thickness(2)
    .color(ui.rgb(0, 255, 140))
    .range(0, 100);
  • drawGraphSamples() рисует переданный массив сразу, не накапливает внутреннюю историю точек. Для streaming-режима с накоплением используйте drawGraphLine()
  • updateGraphSamples() использует тот же API, но подходит для локального in-place обновления

Для Oscilloscope эти настройки задаются прямо у сетки:

ui.drawGraphGrid()
    .pos(center, center)
    .size(200, 170)
    .direction(Oscilloscope)
    .scope(2000, 100)                  // частота входных samples и длительность окна в мс
    .visible(0);

Что важно по lifecycle:

  • drawGraphGrid() задаёт активную область графика и должен вызываться в screen-callback этого экрана
  • buffered-график (drawGraphLine()) живёт только пока экран реально рисует граф в текущих кадрах
  • если экран перестал вызывать graph API или вы ушли на другой screen, внутренние буферы графа освобождаются
  • при возврате граф начинает собирать историю заново

Что важно по режимам:

  • LeftToRight и RightToLeft используют rolling-history
  • Oscilloscope использует фиксированное окно видимых samples
  • если visible(0), окно для Oscilloscope вычисляется из rateHz * timebaseMs

15.4. Пауза графиков (freeze)

В 3-button режиме, библиотека поддерживает заморозку графика:

  • короткое нажатие Select на экране, где рисуется граф, переключает pause/resume только для графика этого экрана;
  • во время паузы новые значения/массивы для графиков игнорируются, а на экране остаётся последний отрисованный кадр графа.
bool paused = ui.graphPaused();                 // текущее состояние
bool toggled = ui.GraphPauseToggled();          // одноразовое событие (true один раз после переключения)

Важно:

  • механизм включён только когда реально есть 3-я кнопка (3-button режим);

16. Уведомления, toast, ошибки

16.1. Toast

ui.showToast()
    .text("Saved")    // основной текст toast; можно оставить пустым, если нужен только значок
    .icon(error)      // необязательная иконка слева
    .pos(top);        // `top` или `down`; по умолчанию используется `down`

Позиции toast:

  • top — появляется сверху
  • down — появляется снизу

Проверка активности:

bool active = ui.toastActive();    // `true`, пока toast еще анимируется или висит на экране

Поведение:

  • если не заданы и текст, и иконка, toast не показывается
  • toast сам показывается, держится на экране и затем скрывается с анимацией

16.2. Notification

ui.showNotification()
    .text("Settings", "Changes applied") // заголовок и основной текст карточки
    .button("OK")                        // подпись кнопки подтверждения
    .delay(0)                            // автозакрытие в секундах; `0` значит без таймера
    .type(Normal)                        // semantic-тип уведомления
    .icon(warning);                      // необязательная иконка; без нее библиотека берет visual от `type(...)`

Типы уведомления:

  • Normal
  • Warning
  • Error

Управление:

bool active = ui.notificationActive();

16.3. Popup menu

Самый обычный сценарий:

static const char *menuItems[] = {"Edit", "Rename", "Delete"};

auto button = ui.updateButton()
    .label("Open menu")
    .pos(center, 188)
    .size(180, 32)
    .baseColor(ui.rgb(0, 87, 250))
    .radius(10);

ui.showPopupMenu()
    .items(menuItems)                  // обычный массив строк
    .anchor(button)                    // привязка к fluent-компоненту; меню само откроется над ним или под ним
    .width(120)                        // ширина меню; если не задать, библиотека подберет сама
    .selected(0);                      // стартовый выделенный пункт; если не задавать, берется дефолт

Результат выбора:

if (ui.popupMenuHasResult())                      // говорит, что пользователь уже выбрал пункт
{
    int16_t picked = ui.popupMenuTakeResult();    // возвращает индекс выбранного пункта и сразу сбрасывает флаг результата
}

Что важно:

  • .selected(index) нужен только если хочешь вручную задать стартовый курсор
  • .anchor(component) берет прямоугольник fluent-компонента и открывает меню по центру над ним или под ним
  • если использовать короткий паттерн без popupMenuHasResult(), то popupMenuTakeResult() == -1 значит, что результата пока нет

Выбор пункта:

  • 2-button режим: удержание NEXT подтверждает текущий пункт; удержание PREV закрывает меню без выбора
  • 3-button режим: короткое нажатие SELECT подтверждает текущий пункт; удержание PREV закрывает меню без выбора

16.4. Ошибки

ui.showError()
    .message("Low battery")
    .code("0xLOWBAT")
    .type(Warning)
    .button("OK");

ui.showError()
    .message("LittleFS mount failed")
    .code("0xLFS")
    .type(Error)
    .button("Retry");

Управление:

ui.nextError();                                                                 // переключает на следующую ошибку
ui.prevError();                                                                 // переключает на предыдущую ошибку
ui.errorActive();                                                               // сообщает, активен ли сейчас error overlay
ui.setErrorButtonDown(btnOk.isDown());                                          // короткий совместимый wrapper для простого сценария с одной кнопкой
ui.setErrorButtonsDown(btnNext.isDown(), btnPrev.isDown(), btnCombo.isDown());  // полный input API для error overlay

Поведение:

  • если ошибок несколько, первой показывается последняя добавленная
  • Warning можно закрыть
  • Error не закрывается пользовательской кнопкой

17. Скриншоты

17.1. Build-time флаги

  • PIPGUI_SCREENSHOTS
    • 1 по умолчанию
    • 0 полностью выключает систему скриншотов
  • PIPGUI_SCREENSHOT_MODE
    • 1 — serial capture
    • 2 — запись в LittleFS

Для режима 2 нужен LittleFS. Библиотека сама создаёт /PipKit/, /PipKit/screenshots/ и /PipKit/thumbnails/.

17.2. Serial capture

Скрипт на ПК:

python Tools/Screenshots/Bin/Capture.py
  • при запуске без параметров tool покажет меню: port, baud и output directory

Быстрый прямой запуск тоже можно:

python Tools/Screenshots/Bin/Capture.py --port COM9 --baud 1000000

17.3. Flash (LittleFS)

В режиме 2 сохраняются:

  • full screenshot: /PipKit/screenshots/pscr_00000001.pscr
  • thumbnail: /PipKit/thumbnails/<WxH>/pscr_00000001.pscr

Галерея показывает новые сверху.

17.4. Запуск скриншота

bool started = ui.startScreenshot();
  • startScreenshot() запускает захват асинхронно
  • если snapshot-buffer не удалось выделить, функция вернёт false
  • built-in shortcut фиксированный: удержание Next + Prev 300 ms

17.5. Галерея миниатюр

Отрисовка галереи:

ui.drawScreenshot()
    .pos(8, 28)     // x, y области галереи
    .size(224, 284) // w, h области галереи
    .grid(3, 5)     // cols, rows видимой сетки
    .padding(8);    // отступ между ячейками в самой сетке

Что делает drawScreenshot():

  • рисует текущую screenshot gallery в заданной области
  • сам берёт entries из внутреннего screenshot store
  • сам вычисляет размер миниатюр и вместимость галереи из size(...), grid(...) и padding(...)
  • в serial mode просто покажет пустое состояние, если gallery backend не используется

Дополнительно:

uint8_t count = ui.screenshotCount();

18. Wi‑Fi

18.1. Build-time флаги

  • PIPGUI_WIFI
    • 1GUI::loop() обслуживает standalone Wi‑Fi wrapper
    • 0 — standalone Wi‑Fi path не обслуживается автоматически
  • PIPGUI_WIFI_SSID
  • PIPGUI_WIFI_PASSWORD
  • PIPCORE_ENABLE_WIFI
    • должен быть 1, если используется внешний или внутренний Wi‑Fi API PipCore

Важно:

  • PIPGUI_WIFI и PIPCORE_ENABLE_WIFI это не одно и то же
  • PIPGUI_WIFI включает GUI-level сценарии и auto-service в GUI::loop()
  • PIPCORE_ENABLE_WIFI включает сам backend в PipCore
  • если GUI или прошивка вызывают pipcore::net::*, а PIPCORE_ENABLE_WIFI=0, будет compile-time ошибка
  • OTA продолжает работать и при PIPGUI_WIFI = 0
  • у OTA свой runtime-path, он не зависит от standalone Wi‑Fi servicing в GUI::loop()

18.2. API

ui.requestWiFi(true);   // включить или выключить standalone Wi‑Fi request
ui.wifiState();         // текущее состояние backend-а
ui.wifiConnected();     // true только в Connected
ui.wifiLocalIpV4();     // IPv4 как packed uint32_t

19. OTA

Для OTA нужна A/B partition table с двумя OTA app-слотами.

19.1. Тулинг (Tools/Ota/)

Сгенерировать ключ:

python Tools/Ota/Key.py

Если запустить без параметров, tool покажет меню и даст выбрать путь сохранения числом.

Сделать stable release:

python Tools/Ota/Release.py

Сделать beta release:

python Tools/Ota/Release.py --beta --interactive

Проверка manifest + bin:

python Tools/Ota/Verify.py

При запуске без параметров tool покажет меню: manifest, firmware source и signature check.

19.2. Конфигурация

Обычно всё задаётся в include/config.hpp:

#define PIPCORE_ENABLE_WIFI 1
#define PIPCORE_ENABLE_OTA 1
#define PIPCORE_OTA_PROJECT_URL "https://example.com/fw/pipGUI"

#define PIPGUI_OTA 1
#define PIPGUI_OTA_PROJECT_URL "https://example.com/fw/pipGUI"
#define PIPGUI_OTA_ED25519_PUBKEY_HEX "..."

#define PIPGUI_FIRMWARE_TITLE "PipKit"
#define PIPGUI_FIRMWARE_VERSION "1.6.2"

Что это значит:

  • PIPCORE_ENABLE_WIFI включает core-level Wi‑Fi backend, без него OTA API PipCore недоступен
  • PIPCORE_ENABLE_OTA включает core-level OTA backend
  • PIPCORE_OTA_PROJECT_URL это URL, который использует сам PipCore OTA backend
  • PIPGUI_OTA включает сам OTA subsystem
  • PIPGUI_OTA_PROJECT_URL это базовый URL проекта, откуда берутся manifest и бинарники
  • PIPGUI_OTA_ED25519_PUBKEY_HEX это публичный ключ, которым проверяется подпись manifest и firmware
  • PIPGUI_FIRMWARE_TITLE это имя прошивки в UI
  • PIPGUI_FIRMWARE_VERSION это текущая версия прошивки в формате "major.minor.patch"

Что обычно важно:

  • если GUI использует OTA, то мало включить только PIPGUI_OTA, нужно ещё включить PIPCORE_ENABLE_WIFI=1 и PIPCORE_ENABLE_OTA=1
  • без PIPGUI_OTA_ED25519_PUBKEY_HEX нормальная защищенная OTA-схема не имеет смысла
  • URL и версия это то, что меняют чаще всего
  • если OTA не нужен, достаточно держать PIPGUI_OTA 0

19.3. Использование

Минимальный сценарий

ui.otaConfigure();                    // один раз на старте
ui.otaRequestCheck();                 // запустить проверку обновления

const OtaStatus& st = ui.otaStatus(); // текущее состояние OTA state-machine

Этого уже хватает для обычного сценария, потому что GUI::loop() сам обслуживает OTA backend.

Проверка обновления

ui.otaRequestCheck();
ui.otaRequestCheck(NewerOnly);
ui.otaRequestCheck(AllowDowngrade);
  • ui.otaRequestCheck() это обычная проверка
  • NewerOnly разрешает только обновление вверх
  • AllowDowngrade разрешает откат на более старую версию
  • backend идет по каналам в порядке stable -> beta
  • если в stable ничего подходящего нет, потом проверяется beta

Установка и отмена

ui.otaRequestInstall();   // начать установку уже найденного релиза
ui.otaCancel();           // отменить текущую OTA-операцию

История stable-версий

ui.otaRequestStableList();                    // запросить список stable-версий
bool ready = ui.otaStableListReady();         // список уже загружен
uint8_t count = ui.otaStableListCount();      // сколько stable-версий доступно
const char* ver = ui.otaStableListVersion(i); // строка версии по индексу

ui.otaRequestInstallStableVersion("1.6.2");

Это нужно только если хочешь показывать пользователю список старых stable-сборок и давать выбрать rollback вручную.

Дополнительно

ui.otaService();
ui.otaMarkAppValid();
  • otaService() публичный, но обычно вручную не нужен, потому что GUI::loop() уже вызывает его сам
  • otaMarkAppValid() нужен после reboot, если новая прошивка стартовала в режиме ожидания подтверждения
  • в обычном UI-коде чаще всего достаточно otaConfigure(), otaRequestCheck(), otaRequestInstall() и otaStatus()

Ошибки OtaError

  • None
  • WifiNotEnabled
  • WifiNotConnected
  • HttpBeginFailed
  • HttpStatusNotOk
  • ManifestTooLarge
  • ManifestParseFailed
  • ManifestReplay
  • SignatureMissing
  • SignatureInvalid
  • FlashLayoutInvalid
  • RollbackUnavailable
  • UpdateBeginFailed
  • UpdateWriteFailed
  • HashPipelineFailed
  • DownloadTruncated
  • PayloadSizeMismatch
  • HashMismatch
  • UpdateEndFailed
  • UrlTooLong

На практике чаще всего встречаются:

  • WifiNotConnected — нет соединения
  • HttpStatusNotOk — сервер не отдал корректный HTTP-ответ
  • ManifestParseFailed — manifest битый или неожиданный по формату
  • SignatureInvalid — подпись не сошлась
  • HashMismatch — firmware скачалась, но checksum не совпала