Install arduino-cli before running the project scripts.
Setup
Getting Started
Install arduino-cli and esptool first. After arduino-cli is available, run bin/all_libs.sh to install the ESP32 Arduino core and the libraries used by the firmware.
Before flashing hardware, find the serial port and set PORT in bin/config.sh.
Install esptool before uploading firmware.
bin/all_libs.shRuns the project toolchain setup after arduino-cli is installed.
ls /dev/cu.usbmodem* /dev/cu.usbserial* 2>/dev/nullUse the matching device path as PORT in bin/config.sh.
$EDITOR bin/config.shSet PORT, UPLOAD_SPEED, and ESPTOOL for your machine. Keep the ESP32-S3 FQBN unless the board target changes.
Build note
Build and Flash
tools/container-compile.sh exists mainly as a fast syntax check for agentic development. It is useful when you want a quick compile sanity pass without flashing hardware.
bin/app.sh generates wrapper files for nested sources, builds src/src.ino, checks the binary size and partition layout, then uploads with esptool.
tools/container-compile.shFast syntax-only validation. Useful for quick checks; it is not the normal hardware flashing path.
bin/app.shSupports ENABLE_NETWORK_APPS=0 for a local-only build, ENABLE_TTS=1 or WITH_TTS=1 to include TTS, NODEBUG=1 for a smaller release-oriented build, and VERBOSE=1 for verbose tool output.
bin/app.sh uses arduino-cli for the build and esptool for the upload.
tools/generate-secrets-header.sh creates src/secrets_config.h from .env when present, otherwise from .env.example.
Runtime
Configuration and State
Firmware defaults are compiled in first, then configReload() overlays /cfg/system.cfg from the SD card when the file exists.
Per-app settings are stored under /cfg/app-<id>.cfg. Active apps can also serialize session state before deep sleep and restore it after wake.
Periodic-task policy is configured with periodic.minIntervalMinutes, periodic.cpuMhz, periodic.wifiCpuMhz, periodic.disableOnLowBattery, and periodic.lowBatteryPercent. Network time-sync cadence uses time.syncIntervalHours; zero disables the OS task and the default is 24 hours.
The currently supported system keys cover Wi-Fi, CPU governor and frequency, time format and sync settings, sensor.tempOffsetC, and home.sunBtnApp.
The sleep clock keeps a small wake snapshot in RTC memory, and app runtimes can use save/restore hooks to preserve active session bytes across deep sleep. If the hardware RTC cannot be read, retained state supplies an estimated time and the lockscreen marks it with a line above the clock.
App hooks request a cadence from one minute through one day. The OS clamps it globally, suppresses hooks under the configured low-battery policy, and keeps the sleep-clock update independent.
Runtime
Internal App Lifecycle
Built-in apps are registered as AppDefinition entries with a screen id, runtime object, optional launch and reset hooks, and an AppBehavior table.
launchApp() runs reset(), then launch(), then calls switchTo() with the app runtime. When app ownership changes, switchTo() calls the previous app's onRelease() hook, and calls onExit() unless the app is only being paused for the quit dialog, before calling the next app's onEnter() hook.
Menu and power button single, double, and long presses are dispatched through AppBehavior and return AppEventResult values such as Dirty, GoHome, GoMenu, or QuitRequested.
Apps normally receive press coordinates through ActiveApp::handleTouch(). Apps that need down, move, and up events can attach onRawTouch; the shell then drains TouchInput::readEvent() directly for that app.
Before deep sleep, the shell stores the active app id, menu state, Wi-Fi state, local clock snapshot, and up to 4096 bytes from onSaveContext(). After wake, it relaunches the app and passes those bytes to onRestoreContext().
Each app can override the default inactivity deep-sleep timeout through AppDefinition::inactivityDeepSleepMs.
Register one hook with BehaviorBuilder::withPeriodic(). PeriodicTaskContext reports Basic, System, or Network capability; the hook may complete, escalate once, wake the device, or wake its app. Display initialization is selected separately as none, partial, or full.
OS services use periodicSystemTaskRegister() and the same scheduler. Time synchronization is registered at the Network tier using the configurable 0, 1, 3, 6, 12, or 24 hour interval; 0 means never and 24 hours is the default.
A bounded RTC table keeps 6-byte aggregates: task hash, cumulative 10 ms duration units, and run count. Callback profiling does not initialize USB or resample battery hardware.
Hardware layer
Hardware Notes
200x200 monochrome panel on SPI2 with a 5000-byte framebuffer. The board wiring is EPD_CS_PIN=11, EPD_DC_PIN=10, EPD_RST_PIN=9, EPD_BUSY_PIN=8, EPD_SCK_PIN=12, EPD_MOSI_PIN=13, and EPD_PWR_PIN=6.
FT6336-compatible touch controller on I2C0 address 0x38 with ESP32_I2C_SDA=47, ESP32_I2C_SCL=48, EPD_TP_RST_PIN=7, and EPD_TP_INT_PIN=21. A 5 ms FreeRTOS task polls and queues down, move, and up events.
BOOT_BUTTON_PIN is GPIO0 and PWR_BUTTON_PIN is GPIO18.
The optional PCF85063 RTC uses the same I2C bus at address 0x51. Deep sleep wake uses the power button on GPIO18, and the firmware can keep the display rail retained for the sleep-clock path.
SHTC3 temperature and humidity sensor on I2C address 0x70.
ESP chip temperature is read through the ESP-IDF temperature sensor API when available.
Battery sampling uses BAT_ADC_PIN=4. BAT_CTRL_PIN=17 keeps the board power latch alive for normal operation and sleep entry.
ES8311 codec on I2C address 0x18 with I2S0 on MCLK=14, BCLK=15, DIN=16, WS=38, and DOUT=45. AUDIO_PWR_PIN=42 controls codec power and PA_CTRL_PIN=46 controls the speaker amp.
The board uses 1-bit SD_MMC wiring on CLK=39, D0=40, and CMD=41.
Extension path
External Apps
External .pink apps are scanned from /bin/games, /bin/apps, and /bin/network when the network category is compiled in.
tools/build-pink-app.sh examples/pink/tetris/tetris.cpp build/pink/tetris.pinkBuilds the sample external app.
The runtime accepts .pink ABI versions 7 through 12. tools/build-pink-app.sh emits ABI version 12; rebuild external apps to use PINK_PERIODIC_TASK(...) and PINK_EVENT_PERIODIC.
Writable data and BSS are not relocated in the current .pink format. Keep mutable app state in the runtime memory block instead of global or static writable objects.
External apps include examples/pink/include/pink_app_api.h and use the helpers in examples/pink/include/pink_runtime.cpp.
A .pink app may declare one System- or Network-tier hook from one minute through one day with PINK_PERIODIC_TASK(...). The schedule is packed into the executable, so SD removal, deletion, and rebuilds cannot leave stale registrations; periodic events may request a normal device or foreground-app wake.
Extension path
PocketInkJS Apps
PocketInkJS provides a lightweight app path for scripts stored on the SD card. It exists for rapid iteration and user-created apps that should not require the native .pink cross-compilation and ABI toolchain.
A PocketInkJS app participates in the normal shell lifecycle and can build interactive interfaces, use system-owned pickers, access bounded storage and network services, work with sensors and audio, and preserve app state through the host facade. The firmware retains ownership of hardware, permissions, modal UI, memory limits, and cleanup.
See the PocketInkJS API reference for a complete listing of available system APIs.