Skip to content

Instantly share code, notes, and snippets.

@mkasberg
Created August 20, 2026 03:52
Show Gist options
  • Select an option

  • Save mkasberg/493f3ab912a5536f3ca28531c51abce6 to your computer and use it in GitHub Desktop.

Select an option

Save mkasberg/493f3ab912a5536f3ca28531c51abce6 to your computer and use it in GitHub Desktop.
AGENTS.md for InfiniSim/InfiniTime

AGENTS.md — Working with InfiniSim (+ InfiniTime)

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.

Repository layout

  • InfiniSim (this repo) — the LVGL/SDL simulator. Builds build/infinisim (the sim) and build/littlefs-do (a CLI for the emulated SPI flash image spiNorFlash.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 in InfiniTime/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").

First-time setup (Ubuntu)

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_conv once installed into ~/node_modules instead of the repo (no package.json here). Fix: mkdir -p node_modules/.bin && ln -sf ~/node_modules/.bin/lv_font_conv node_modules/.bin/lv_font_conv.
  • If pkg-config is missing, SDL2/libpng checks may misleadingly report "not found" even when the -dev packages are installed. Trust CMake's configure output.
  • No display server? The binary still builds and --help works; run the GUI on a desktop.

Build and run the simulator

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/B bluetooth on/off, n/N send/clear notification, v/V battery ±10%, c/C charging on/off, s/S steps ±500, h/H heartrate, i screenshot, w/W weather data, arrows = swipe gestures. See README.md for all.

External resources (fonts/images) — required!

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   # verify

Gotcha: 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.

CMake cache gotcha

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)

Taking a screenshot of the running sim (X11)

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.)

Adding / iterating on a watch face

Checklist (used for the "Casio2" face, copied from WatchFaceDigital):

  1. Copy WatchFaceX.h/.cpp in InfiniTime/src/displayapp/screens/, rename class.
  2. Register in four places:
    • InfiniTime/src/displayapp/apps/Apps.h.in — add to WatchFace enum
    • InfiniTime/src/displayapp/apps/CMakeLists.txt — add WatchFace::X to defaults
    • InfiniTime/src/displayapp/UserApps.h — include the header
    • InfiniTime/src/CMakeLists.txt — add the .cpp to sources
  3. Clean cmake configure (see cache gotcha above), build, load resources, run.

Useful patterns:

  • Fonts are build-time bitmaps (InfiniTime/src/displayapp/fonts/fonts.json for compiled-in; InfiniTime/src/resources/fonts.json for external-flash .bin fonts via lv_font_conv). No runtime scaling — add a new size entry (e.g. 7segments_60) and load with lv_font_load("F:/fonts/..."), free in destructor, check in IsAvailable().
  • Images: put PNG in InfiniTime/src/resources/images/, register in resources/images.json; the build auto-converts to .bin. Load with lv_img_set_src(img, "F:/images/foo.bin"). For a full-screen opaque background, CF_TRUE_COLOR (RGB565, 2 B/px) is smaller than CF_TRUE_COLOR_ALPHA — support for it was added to InfiniTime/src/resources/lv_img_conv.py in 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 in screens/Symbols.h (bluetooth, bell, plug, bolt, shoe...).
  • Coordinates: origin top-left, x+ right, y+ down; screen is 240×240.

Deploying to a real watch (PineTime)

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.

Build the firmware with Docker (avoids ARM toolchain/nRF5 SDK setup)

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-app

Outputs 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).

Flash both over BLE from Ubuntu with itd/itctl

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.zip

Useful 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.

General notes for agents

  • 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.
  • InfiniTime is 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment