Этот файл описывает актуальный публичный API PipGUI и PipCore, который есть в коде проекта.
PipKit использует C++17. Для PlatformIO добавьте в platformio.ini:
build_unflags =
-std=gnu++11
build_flags =
-std=gnu++17Низкий слой 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_PREFS0или1
PIPCORE_ENABLE_WIFI0или1
PIPCORE_ENABLE_OTA0или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_*тоже должны быть включены явно
Симулятор нужен для локального прогона GUI на Windows/Linux.
Он собирает проект в desktop-конфигурации и подменяет platform/display слой на DESKTOP + SIMULATOR.
Windows:
powershell -ExecutionPolicy Bypass -File .\Tools\Simulator\Windows\Sim.ps1Linux:
./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/
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
- fallback-ширина экрана симулятора, если проект не вызвал
PIPGUI_SIM_DEFAULT_HEIGHT- fallback-высота экрана симулятора, если проект не вызвал
configDisplay().size(...) - по умолчанию
320
- fallback-высота экрана симулятора, если проект не вызвал
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 21LeftилиA- кнопкаPrevRightилиD- кнопкаNextEnterилиSpace- кнопкаSelect- клавиши
0..3отправляются в serial input simulator-а как обычные символы
В правой панели доступны настройки:
- пауза/возобновление
- шаг кадра вперёд и назад, с настраиваемым количеством кадров за один шаг
Time scaleslider для замедления/ускорения simulator-clockSPI 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 оставлен включённым для совместимости сборки
Если проект пишет графику через PipCore / PipGUI, для simulator обычно достаточно следующего:
config_sim.hpp, где выбраныPIPCORE_PLATFORM DESKTOPиPIPCORE_DISPLAY SIMULATOR- host include path, чтобы компилятор видел
PipCore/Host/Desktopcompat-layer - desktop build script или CMake, который собирает:
- код
PipCore - код самого проекта
PipCore/Host/Desktop/src/Runner.cppPipCore/Host/Desktop/src/Globals.cpp
- код
Что важно:
- если UI-код опирается только на
PipCore/PipGUI, этого достаточно для графической симуляции - если проект использует прямые ESP32-only вызовы, для них всё равно нужны desktop stub/compat-реализации или абстракция через
PipCore
Полный пример:
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по умолчанию80000000ILI9488по умолчанию60000000
order("RGB"):ST7789по умолчаниюRGBILI9488по умолчаниюBGR
invert(bool)по умолчаниюtrueswap(bool)по умолчаниюfalseoffset(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);После конфигурации дисплея вызывается 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().
ui.setRotation(0); // повернуть экран сразу или с анимацией по умолчанию
ui.setRotation(1, 620); // тот же поворот, но со своей длительностью
ui.screenRotation(); // текущая ориентация 0..3
ui.rotationTransitionActive(); // идёт ли сейчас анимация переворотаsetRotation(...)меняет ориентацию уже послеbegin(), без повторногоconfigDisplay()- библиотека держит физическую конфигурацию дисплея как есть и поворачивает GUI логически, пересобирая внутренний sprite под новый размер
screenRotation()возвращает текущий runtime rotation0..3rotationTransitionActive()позволяет не запускать свой второй переход поверх уже идущего переворота
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(...)
void myBacklight(uint16_t level) {
// свой способ применить яркость
}
ui.setBacklightHandler(myBacklight);setBacklightHandler(...)позволяет подставить свой handler для управления подсветкой- callback имеет сигнатуру
void (*)(uint16_t level) levelэто целевой уровень яркости, который библиотека хочет применить- это low-level хук для своего драйвера подсветки, если стандартного
setBacklight()недостаточно - обычно этот API не нужен, если хватает
setBacklight()
ui.showLogo()
.text("PISPPUS", "Digital Thermometer")
.anim(FadeIn);showLogo() запускает полноэкранную boot-анимацию с логотипом.
Размер текста библиотека подбирает сама под текущее разрешение экрана.
Параметры:
text(title, subtitle)— две строки логотипа;anim(...)—None,FadeIn,LightFade;
Что делают анимации:
None- просто сразу показывает логотип без анимацииFadeIn- плавно проявляет текст из цвета фонаLightFade- плавно поднимает яркость подсветки; для неё backlight должен быть заранее настроен
ui.screenWidth(); // ширина вашего экрана
ui.screenHeight(); // высота вашего экранаscreenWidth()иscreenHeight()возвращают уже активный логический размер экрана- удобно для центровки, адаптивных отступов и расчёта layout без хардкода
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
ui.clear(ui.rgb(0, 0, 0));- очищает весь текущий draw target указанным цветом
ui.setClip()
.pos(10, 20)
.size(120, 80);
// ... рисование только внутри области ...
ui.clearClip();setClip()ограничивает всю последующую отрисовку прямоугольной областью- удобно для локальных redraw, списков, анимаций и виджетов в карточках
clearClip()возвращает обычную отрисовку без ограничений
У 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 становится шаблоном и сам больше не коммитится. Рисуется уже производная цепочка или финальная донастройка исходного объекта.
ui.setAdaptivePreview(240, 135, 7200);
ui.clearAdaptivePreview();- это debug-helper для проверки адаптивности интерфейса
setAdaptivePreview(minWidth, minHeight, cycleMs)плавно гоняет логический размер GUI между текущим физическим экраном и указанным минимумом- при этом
screenWidth()иscreenHeight()реально меняются, поэтому layout пересчитывается по-настоящему - это не замена
configDisplay().size(...)и не перенастройка самой панели: физический дисплей остаётся тем же, меняется только логический viewport GUI clearAdaptivePreview()выключает этот режим и возвращает обычный размер экрана
Это 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()};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(...)
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);Для распределения доступны:
StartCenterEndlayout::SpaceBetweenlayout::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);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,RightTop,Bottom
Текущий шрифт настраивается отдельными методами:
ui.setFont(WixMadeForDisplay); // выбирает семейство шрифта
ui.setFontSize(18); // задаёт текущий размер в пикселяхВ библиотеке сейчас есть два встроенных семейства:
WixMadeForDisplayKronaOne
Для веса можно передавать либо число, либо готовый токен:
ui.setFontWeight(450); // обычно это диапазон `100..900`
ui.setFontWeight(Semibold);ThinLightRegularMediumSemiboldBoldBlack
Текущее состояние тоже можно читать:
ui.fontId(); // текущий FontId
ui.fontSize(); // текущий размер шрифта
ui.fontWeight(); // текущая толщина шрифтаui.setTextStyle(H1);setTextStyle(...) быстро выставляет готовый пресет под тип текста.
Доступные стили:
H1- крупный заголовокH2- подзаголовок или вторичный заголовокBody- основной текст интерфейсаCaption- мелкие подписи и пояснения
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)центрирует строку как объект по обеим осям
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)задаёт выравнивание строк внутри области
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(...) и добавляет многоточие
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, потом рисует новый кадр
Чтобы добавить свою иконку:
- положить source-файл в
Tools/Icons/Sources/ - пересобрать проект — генератор сам обновит готовые файлы в
lib/PipKit/PipGUI/Graphics/Text/Icons/ - использовать иконку по имени файла
Пример:
- файл
Tools/Icons/Sources/checkmark.svg - в коде:
.icon(checkmark)
Что важно:
- генератор читает и
.svg, и.jsonрекурсивно - файлы могут лежать как прямо в корне
sources, так и в любых подпапках - имя для C++ генерируется из относительного пути, так что вложенные папки тоже поддерживаются
Что появится в коде:
- для обычной однослойной иконки из
name.svgпоявляется aliasname - для многослойной иконки появляются alias вида
name_l0,name_l1,name_l2 - дополнительно экспортируются и enum-константы
IconName,IconNameL0,IconNameL1и т.д. - для анимированной иконки из
name.jsonпоявляется aliasname_anim
Если в имени файла есть -, пробелы или другие неподходящие символы, генератор сам приводит имя к валидному C++-идентификатору через _.
.svgидёт в обычный static PSDF pipeline..jsonидёт в animated PSDF pipeline.
Что важно:
fill(color565)— задаёт заливку; если не вызывать, фигура остаётся без заливкиborder(widthPx, color565)— задаёт контур; если не вызывать, контура не будетfill(...)иborder(...)можно использовать вместе или по отдельности
ui.drawLine()
.from(20, 20) // начало линии
.to(140, 60) // конец линии
.thickness(2) // толщина линии; по умолчанию `1`
.color(ui.rgb(255, 255, 255)) // цвет линии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)) // толщина и цвет контураui.drawCircle()
.pos(50, 50) // центр круга
.radius(18) // радиус
.fill(ui.rgb(0, 87, 250)) // цвет заливки
.border(1, ui.rgb(255, 255, 255)) // толщина и цвет контураui.drawEllipse()
.pos(120, 50) // центр эллипса
.radiusX(28) // горизонтальная полуось
.radiusY(16) // вертикальная полуось
.fill(ui.rgb(255, 0, 72)) // цвет заливки
.border(1, ui.rgb(255, 255, 255)) // толщина и цвет контура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); // основание внизу, вершина сверхуui.drawArc()
.pos(100, 80) // центр дуги
.radius(28) // внешний радиус
.thickness(6) // толщина; по умолчанию 1
.start(-90.0f) // начальный угол в градусах
.end(90.0f) // конечный угол в градусах
.color(ui.rgb(80, 255, 120)) // цвет дугиУ дуги концы всегда скруглённые, а диапазон 0..360 рисует полный круг.
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)) // толщина и цвет контураУ всех градиентов pos(...) и size(...) задают прямоугольную область рисования.
ui.gradientVertical()
.pos(20, 20)
.size(120, 40)
.TColor(ui.rgb(255, 0, 72))
.BColor(ui.rgb(0, 87, 250))- цвет плавно меняется сверху вниз
ui.gradientHorizontal()
.pos(20, 70)
.size(120, 40)
.LColor(ui.rgb(255, 128, 0))
.RColor(ui.rgb(80, 255, 120))- цвет плавно меняется слева направо
ui.gradientDiagonal()
.pos(20, 170)
.size(120, 40)
.TLColor(ui.rgb(255, 255, 255))
.BRColor(ui.rgb(30, 30, 30))- плавный переход по диагонали от верхнего левого к нижнему правому углу
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))- каждая вершина прямоугольника получает свой цвет, внутри идёт двунаправленная интерполяция между четырьмя углами
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(...)задают цвета в центре и на периферии
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) // те же параметры materialupdateBlur() нужен для in-place перерисовки той же blur-области без полной перерисовки экрана.
direction(...) поддерживает:
TopDown- материал и его усиление идут сверху внизBottomUp- материал и его усиление идут снизу вверхLeftRight- материал и его усиление идут слева направоRightLeft- материал и его усиление идут справа налево
material(strength, color) управляет только верхним tinted-слоем поверх blur:
- чем больше
strength, тем заметнее tinted-слой поверх blur color = -1- взять текущий цвет фона библиотеки
То есть blur остаётся тем же, а меняется только то, как поверх него распределяется tinted-слой.
Круг:
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- свечение плавно пульсирует по силе
Экраны регистрируются через макрос 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(...)и другие APIorderзадаёт порядок регистрации экранаorderдолжен быть уникальным для каждого экрана- обычно первый экран делают с
order = 0
После этого экран можно активировать:
ui.setScreen(ScreenHome);Базовый вариант:
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- одноразовое событие нажатия
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-pressRepeat- повторные тики после long-pressReleased- отпускание кнопки
Встроенные handlers:
ui.listNav():Next Released- следующий пунктPrev Released- предыдущий пунктPrev LongPressed-backScreen()Select Releasedв 3-button режиме - открытьtargetScreenNext LongPressedв 2-button режиме - открытьtargetScreenCombo 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, если сам виджет это делает
Эти методы управляют активным экраном и переходами между экранами.
ui.setScreen(ScreenHome); // сразу переключает на указанный экран
ui.currentScreen(); // возвращает id текущего активного экрана
ui.nextScreen(); // переходит на следующий экран по порядку регистрации
ui.prevScreen(); // переходит на предыдущий экран по порядку регистрации (циклически)
ui.backScreen(); // возвращает назад по navigation-history
ui.screenTransitionActive(); // показывает, что переход между экранами сейчас ещё идётЧто важно:
- библиотека сама ведёт history переходов между экранами (для
backScreen()) - если экран меняется через
setScreen(...), текущий экран добавляется в history автоматически
ui.requestRedraw();Нужно, когда данные экрана изменились вне обычного render-flow, и вы хотите гарантированно перерисовать следующий кадр.
ui.setScreenAnim(SlideX, 250);setScreenAnim(mode, durationMs) задаёт стиль и длительность перехода между экранами.
Доступные режимы:
None- переход без анимацииSlideX- горизонтальный слайдSlideY- вертикальный слайд
PIPGUI_STATUS_BAR1включает код статус-бара0вырезает runtime-реализацию, публичные методы остаются no-op
PIPGUI_DEBUG_METRICS1включает debug-режим: библиотека рисует диагностический текст в статус-баре (FPS/время кадра/память и т.п.)0выключено (по умолчанию)
Обычно эти флаги задаются в include/config.hpp.
ui.configStatusBar()
.height(18)
.pos(Top)
.style(Blur);Позиции:
TopBottom
Стили:
Solid— обычная непрозрачная полоса; layout резервирует под неё высотуBlur— блюр-полоса поверх контента; layout не должен откусывать под неё safe area
ui.setStatusBarText()
.left("PipGUI")
.center("12:34")
.right("Wi-Fi");ui.setStatusBarBattery(87, Numeric);
ui.setStatusBarBattery(100, Bar);
ui.setStatusBarBattery(-1, Hidden);BatteryStyle:
HiddenNumericBarWarningBarErrorBar
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- при
Blurhelper возвращает0, потому что layout не должен резервировать fixed-height safe area под blur-панель
Можно повесить отдельную иконку в левый, центральный или правый слот статус-бара:
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-е вместе с правым текстом
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 по краям
Обычная отрисовка:
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-режим приоритетнее.
Снаружи нужен только обычный 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));Слайдер подходит для настроек вроде громкости, яркости и подобных значений.
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(...).
Поведение:
- удержание кнопки сначала двигает значение обычным шагом, потом ускоряет и частоту, и величину шага;
- по умолчанию бегунок белый, трек темнее активной части.
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- прижать текст к левому краю progressCenter- выровнять текст по центру progressRight- прижать текст к правому краю progress
anim(...) у progress поддерживает:
None- progress статичный, без анимацииShimmer- по заполненной части идет мягкий движущийся блик
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Горизонтальный:
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(); // включить вертикальный режимПрямо в 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); // режим списка
}Режимы списка:
CardsPlain
Поведение:
- короткое отпускание
NEXTпереключает пункт вперёд; - короткое отпускание
PREVпереключает пункт назад; - 2-button режим: удержание
NEXTоткрываетtargetScreenвыбранного пункта; - 3-button режим: короткое нажатие
SELECTоткрываетtargetScreenвыбранного пункта; - удержание
PREVвозвращает на предыдущий экран из navigation-history.
Прямо в 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.
Режимы плитки:
TextOnlyTextSubtitle
Если обычной сетки мало, можно задать свою понятную раскладку:
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(...)уже не влияет на раскладку
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);ui.drawGraphLine()
.line(0)
.value(sensorValue)
.thickness(2) // задаёт толщину линии графика; по умолчанию `1`
.color(ui.rgb(0, 255, 140))
.range(0, 100);
.scale(true); // вкл/выкл автоматический диапазон по данным для этой graph-areadrawGraphLine()добавляет новую точку в уже настроенный графикupdateGraphLine()подходит для in-place обновления, когда графику нужно самому зачистить и перерисовать нужную область
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-historyOscilloscopeиспользует фиксированное окно видимых samples- если
visible(0), окно дляOscilloscopeвычисляется изrateHz * timebaseMs
В 3-button режиме, библиотека поддерживает заморозку графика:
- короткое нажатие
Selectна экране, где рисуется граф, переключает pause/resume только для графика этого экрана; - во время паузы новые значения/массивы для графиков игнорируются, а на экране остаётся последний отрисованный кадр графа.
bool paused = ui.graphPaused(); // текущее состояние
bool toggled = ui.GraphPauseToggled(); // одноразовое событие (true один раз после переключения)Важно:
- механизм включён только когда реально есть 3-я кнопка (3-button режим);
ui.showToast()
.text("Saved") // основной текст toast; можно оставить пустым, если нужен только значок
.icon(error) // необязательная иконка слева
.pos(top); // `top` или `down`; по умолчанию используется `down`Позиции toast:
top— появляется сверхуdown— появляется снизу
Проверка активности:
bool active = ui.toastActive(); // `true`, пока toast еще анимируется или висит на экранеПоведение:
- если не заданы и текст, и иконка, toast не показывается
- toast сам показывается, держится на экране и затем скрывается с анимацией
ui.showNotification()
.text("Settings", "Changes applied") // заголовок и основной текст карточки
.button("OK") // подпись кнопки подтверждения
.delay(0) // автозакрытие в секундах; `0` значит без таймера
.type(Normal) // semantic-тип уведомления
.icon(warning); // необязательная иконка; без нее библиотека берет visual от `type(...)`Типы уведомления:
NormalWarningError
Управление:
bool active = ui.notificationActive();Самый обычный сценарий:
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закрывает меню без выбора
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не закрывается пользовательской кнопкой
PIPGUI_SCREENSHOTS1по умолчанию0полностью выключает систему скриншотов
PIPGUI_SCREENSHOT_MODE1— serial capture2— запись в LittleFS
Для режима 2 нужен LittleFS. Библиотека сама создаёт /PipKit/, /PipKit/screenshots/ и /PipKit/thumbnails/.
Скрипт на ПК:
python Tools/Screenshots/Bin/Capture.py- при запуске без параметров tool покажет меню: port, baud и output directory
Быстрый прямой запуск тоже можно:
python Tools/Screenshots/Bin/Capture.py --port COM9 --baud 1000000В режиме 2 сохраняются:
- full screenshot:
/PipKit/screenshots/pscr_00000001.pscr - thumbnail:
/PipKit/thumbnails/<WxH>/pscr_00000001.pscr
Галерея показывает новые сверху.
bool started = ui.startScreenshot();startScreenshot()запускает захват асинхронно- если snapshot-buffer не удалось выделить, функция вернёт
false - built-in shortcut фиксированный: удержание
Next + Prev300 ms
Отрисовка галереи:
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();PIPGUI_WIFI1—GUI::loop()обслуживает standalone Wi‑Fi wrapper0— standalone Wi‑Fi path не обслуживается автоматически
PIPGUI_WIFI_SSIDPIPGUI_WIFI_PASSWORDPIPCORE_ENABLE_WIFI- должен быть
1, если используется внешний или внутренний Wi‑Fi APIPipCore
- должен быть
Важно:
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()
ui.requestWiFi(true); // включить или выключить standalone Wi‑Fi request
ui.wifiState(); // текущее состояние backend-а
ui.wifiConnected(); // true только в Connected
ui.wifiLocalIpV4(); // IPv4 как packed uint32_tДля OTA нужна A/B partition table с двумя OTA app-слотами.
Сгенерировать ключ:
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.
Обычно всё задаётся в 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 APIPipCoreнедоступенPIPCORE_ENABLE_OTAвключает core-level OTA backendPIPCORE_OTA_PROJECT_URLэто URL, который использует самPipCoreOTA backendPIPGUI_OTAвключает сам OTA subsystemPIPGUI_OTA_PROJECT_URLэто базовый URL проекта, откуда берутся manifest и бинарникиPIPGUI_OTA_ED25519_PUBKEY_HEXэто публичный ключ, которым проверяется подпись manifest и firmwarePIPGUI_FIRMWARE_TITLEэто имя прошивки в UIPIPGUI_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
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-операцию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()
NoneWifiNotEnabledWifiNotConnectedHttpBeginFailedHttpStatusNotOkManifestTooLargeManifestParseFailedManifestReplaySignatureMissingSignatureInvalidFlashLayoutInvalidRollbackUnavailableUpdateBeginFailedUpdateWriteFailedHashPipelineFailedDownloadTruncatedPayloadSizeMismatchHashMismatchUpdateEndFailedUrlTooLong
На практике чаще всего встречаются:
WifiNotConnected— нет соединенияHttpStatusNotOk— сервер не отдал корректный HTTP-ответManifestParseFailed— manifest битый или неожиданный по форматуSignatureInvalid— подпись не сошласьHashMismatch— firmware скачалась, но checksum не совпала