Learnings from hands-on sessions (Aug 2026): environment setup, building/running the simulator, iterating on a watch face, and deploying to a real PineTime watch.
- InfiniSim (this repo) — the LVGL/SDL simulator. Builds
build/infinisim(the sim) andbuild/littlefs-do(a CLI for the emulated SPI flash imagespiNorFlash.raw). - InfiniTime (
InfiniTime/submodule) — the actual watch firmware source. Most UI work (watch faces, screens) happens here; the sim compiles this same code. Watch faces live inInfiniTime/src/displayapp/screens/WatchFace*.cpp. - Two separate builds from the same source: the sim (host g++) and the watch firmware (ARM cross-compile, see "Deploying to a real watch").
sudo apt install -y cmake libsdl2-dev g++ npm libpng-dev
npm install lv_font_conv@1.5.2 # lands in node_modules/.bin, build needs it there
sudo apt install python3-pil # Pillow, for resource.zip generation (or use a .venv)Gotchas encountered:
npm install lv_font_convonce installed into~/node_modulesinstead of the repo (nopackage.jsonhere). Fix:mkdir -p node_modules/.bin && ln -sf ~/node_modules/.bin/lv_font_conv node_modules/.bin/lv_font_conv.- If
pkg-configis missing, SDL2/libpng checks may misleadingly report "not found" even when the-devpackages are installed. Trust CMake's configure output. - No display server? The binary still builds and
--helpworks; run the GUI on a desktop.
cmake -S . -B build
cmake --build build -j$(nproc)
./build/infinisim # add --hide-status to skip the debug window- Mouse: left button = finger (tap/drag), right button = hardware button.
- Keyboard (window focused):
b/Bbluetooth on/off,n/Nsend/clear notification,v/Vbattery ±10%,c/Ccharging on/off,s/Ssteps ±500,h/Hheartrate,iscreenshot,w/Wweather data, arrows = swipe gestures. See README.md for all.
Watch faces that use external fonts/images (Infineat, Casio G7710, Casio2) check for
files on the emulated flash via IsAvailable() and silently disable/hide themselves if
missing. After building, load resources once (and again whenever they change):
./build/littlefs-do res load build/resources/infinitime-resources-1.16.0.zip
./build/littlefs-do ls images # verifyGotcha: res load opens files without LFS_O_TRUNC — a file that shrank keeps stale
trailing bytes. rm it first when replacing: ./build/littlefs-do rm images/foo.bin.
The watch-face list is baked into a generated build/.../apps/Apps.h from
Apps.h.in + the WATCHFACE_TYPES cache variable. After adding a watch face, an
incremental configure may not regenerate it. If a new face doesn't appear in
Settings → Watch face, do a clean configure:
rm -rf build && cmake -S . -B build && cmake --build build -j$(nproc)First, make sure there's only one running window named "TFT Simulator". If more than one are running, you may need to kill/restart so you get the right window.
xwininfo -root -tree | grep -i -E "infini|sdl|sim" # find window id ("TFT Simulator")
import -window 0x1e0000a /tmp/shot.png # ImageMagick 'import'(Or press i in the sim window.)
Checklist (used for the "Casio2" face, copied from WatchFaceDigital):
- Copy
WatchFaceX.h/.cppinInfiniTime/src/displayapp/screens/, rename class. - Register in four places:
InfiniTime/src/displayapp/apps/Apps.h.in— add toWatchFaceenumInfiniTime/src/displayapp/apps/CMakeLists.txt— addWatchFace::Xto defaultsInfiniTime/src/displayapp/UserApps.h— include the headerInfiniTime/src/CMakeLists.txt— add the.cppto sources
- Clean cmake configure (see cache gotcha above), build, load resources, run.
Useful patterns:
- Fonts are build-time bitmaps (
InfiniTime/src/displayapp/fonts/fonts.jsonfor compiled-in;InfiniTime/src/resources/fonts.jsonfor external-flash.binfonts vialv_font_conv). No runtime scaling — add a new size entry (e.g.7segments_60) and load withlv_font_load("F:/fonts/..."), free in destructor, check inIsAvailable(). - Images: put PNG in
InfiniTime/src/resources/images/, register inresources/images.json; the build auto-converts to.bin. Load withlv_img_set_src(img, "F:/images/foo.bin"). For a full-screen opaque background,CF_TRUE_COLOR(RGB565, 2 B/px) is smaller thanCF_TRUE_COLOR_ALPHA— support for it was added toInfiniTime/src/resources/lv_img_conv.pyin these sessions. - Backgrounds: set the screen bg (
lv_obj_set_style_local_bg_color(lv_scr_act(), ...)), don't draw a full-screen rect object. LVGL partial-refresh means a background image does NOT force full-screen redraws — only dirty regions are recomposited. - Reusable widgets:
BatteryIcon(screens/BatteryIcon.h),NotificationIcon::GetIcon(bool), glyphs inscreens/Symbols.h(bluetooth,bell,plug,bolt,shoe...). - Coordinates: origin top-left, x+ right, y+ down; screen is 240×240.
Three separate mechanisms — don't conflate them:
| Artifact | File | Transport |
|---|---|---|
| Firmware | InfiniTime/build/src/pinetime-mcuboot-app-dfu-1.16.0.zip |
OTA DFU (Gadgetbridge, or itctl fw upgrade -a) |
| External resources | InfiniTime/build/src/resources/infinitime-resources-1.16.0.zip |
BLE FS transfer (itctl res load, Amazfish, InfiniLink — not Gadgetbridge) |
| Bootloader | .hex/.bin |
SWD only (wired debug probe); cannot OTA |
A watch face with external resources needs both zips or it stays hidden.
docker pull infinitime/infinitime-build
cd InfiniTime
docker run --rm -v "$PWD:/sources" --user "$(id -u):$(id -g)" \
infinitime/infinitime-build /opt/build.sh pinetime-mcuboot-appOutputs land in InfiniTime/build/src/ (firmware DFU) and build/src/resources/
(resource zip). A post-build cp error about pinetime-mcuboot-recovery-loader is
harmless when only building the app target.
Known build fix (applied in InfiniTime/src/CMakeLists.txt): littlefs's 4-arg
LFS_DEBUG overflows the nRF SDK log macro (LOG_INTERNAL_7 undefined); fixed with
target_compile_definitions(littlefs PRIVATE LFS_NO_DEBUG).
Prereqs: itd daemon running (e.g. systemctl --user start itd), and the phone
disconnected — the watch holds only one BLE connection at a time (disconnect
Gadgetbridge first). Watch must also have file access enabled (watch Settings).
cd InfiniTime
# one shot: firmware + resources
itctl fw upgrade \
-a build/src/pinetime-mcuboot-app-dfu-1.16.0.zip \
-r build/src/resources/infinitime-resources-1.16.0.zip
# or separately:
itctl fw upgrade -a build/src/pinetime-mcuboot-app-dfu-1.16.0.zip
itctl res load build/src/resources/infinitime-resources-1.16.0.zipUseful checks: itctl get address, itctl get battery.
After a firmware OTA, validate on the watch (Settings → Firmware → Validate) or MCUboot rolls back on next reset.
- The user iterates visually: make small position/size tweaks, rebuild
(
cmake --build build -j4), let them look. Don't bundle unrelated changes. - When the user asks a question, answer it — don't change code unless asked.
InfiniTimeis a submodule (may be detached HEAD); watch-face edits live there, sim-only edits (e.g.main.cpp,littlefs-do-main.cpp) live in this repo.