Develop

Code

How the code is organized, and how to add screens, images and fonts

C++17 on PlatformIO, with LVGL 9.2 for the UI. On the board: Arduino-ESP32 and LovyanGFX for the display and touch. In the simulator: SDL2 for the window and libcurl for HTTPS.

Layout#

src/
  main.cpp          The loop: input, App, then LVGL
  app/              App: owns the services and screens; every flow decision lives here
    app.cpp           startup, setup, sign-in, profile, navigation, settings
    app_session.cpp   access tokens, and running requests with a fresh one
    app_beetle.cpp    hunt, cheese, craft, and the cooldowns
    app_icons.cpp     item icons: from flash, the cache, or downloaded
    app_pokes.cpp     poke notifications, the list, poking back
    app_power.cpp     Wi-Fi link, screen off, sleep
    menu_config.h     the home menu's tiles
  ui/               Screen base, the screen stack, theme, shared widgets
  ui/screens/       one class per screen
  account/          remilia.net: sign-in (Keycloak), profile, the beetle game, pokes
  services/         interfaces each platform implements: storage, Wi-Fi, HTTP, setup network
  portal/           the setup web page, shared by the board and the sim
  input/            touch (and the sim's debug keys), as one stream of input events
  media/            images decoded at runtime, buzzer tunes, LED effects
  hal/              hardware primitives (time, display, sleep...)
  platform/esp32/   the board's implementations
  platform/sim/     the simulator's implementations
  util/             Job: runs network and decoding work on a background thread
  fonts/            generated by scripts/fonts.sh (committed)
  assets/           generated from assets/ at build time (not committed)
assets/             images built into the firmware, and their sources
include/            board_config.h (pins), lv_conf.h (LVGL settings)
lib/libwebp/        Google's libwebp, decoder only
scripts/            build steps and asset tools

Only platform/ differs between the board and the simulator. Everything else compiles for both.

Rules#

  • Screens are views. They draw and call App methods. They never touch services or switch screens on their own.
  • Navigation is deferred. The screen stack applies push, pop and replace in its update(), so a screen can close itself from its own button.
  • Each screen has its own focus group, so button focus never leaks between screens.
  • All input goes through Controller. Focusable widgets handle their own clicks. Other input goes to the screen's onInput() first, then to App: Back closes the top screen.
  • Slow work runs in a Job (network, image decoding) on the other core. The main loop polls for the result; jobs never touch LVGL.
  • Tunables go at the top of each file as named constants: sizes, timings, colors, limits.

Adding a screen#

  1. Create src/ui/screens/foo_screen.{h,cpp}: subclass Screen, implement name() and build(). Use the helpers in ui/widgets.h and the colors in ui/theme.h.
  2. Add an openFoo() to App that pushes it: screens_.push(std::make_unique<FooScreen>(*this)).
  3. To put it on the home menu, add a line to src/app/menu_config.h.

Images#

Put PNGs in assets/ (lowercase, digits, _). Each build turns assets/foo.png into img_foo (include assets/assets.h). They're stored as raw screen pixels (RGB565, plus an alpha plane if the PNG has transparency), so export them at the size they're shown at. Numbered files (foo_00.png, foo_01.png...) also get a frame list for animations.

Item icons in assets/item_icons/ are built in too, so common items show instantly. scripts/item_icons.py <request.curl> downloads any new ones. It needs a /cards request copied as cURL from the browser.

Fonts#

scripts/fonts.sh regenerates src/fonts/ from assets/fonts/. It needs Node (for lv_font_conv). Run it when the UI needs new characters or sizes.