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 Arduino core and libraries
bin/all_libs.sh

Runs the project toolchain setup after arduino-cli is installed.

Find the connected port
ls /dev/cu.usbmodem* /dev/cu.usbserial* 2>/dev/null

Use the matching device path as PORT in bin/config.sh.

Edit device target
$EDITOR bin/config.sh

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

Quick syntax check
tools/container-compile.sh

Fast syntax-only validation. Useful for quick checks; it is not the normal hardware flashing path.

Build and upload
bin/app.sh

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

Flashing tools

bin/app.sh uses arduino-cli for the build and esptool for the upload.

Generated secrets header

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.

System config

The currently supported system keys cover Wi-Fi, CPU governor and frequency, time format and sync settings, sensor.tempOffsetC, and home.sunBtnApp.

Sleep and restore

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.

Periodic wake policy

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.

Button routing

Menu and power button single, double, and long presses are dispatched through AppBehavior and return AppEventResult values such as Dirty, GoHome, GoMenu, or QuitRequested.

Touch routing

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.

Sleep persistence

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

Per-app sleep timeout

Each app can override the default inactivity deep-sleep timeout through AppDefinition::inactivityDeepSleepMs.

Periodic hooks

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.

System periodic tasks

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.

Periodic profiling

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

E-paper display

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.

Touch controller

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.

Buttons

BOOT_BUTTON_PIN is GPIO0 and PWR_BUTTON_PIN is GPIO18.

RTC clock

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.

Ambient sensor

SHTC3 temperature and humidity sensor on I2C address 0x70.

Chip temperature

ESP chip temperature is read through the ESP-IDF temperature sensor API when available.

Battery and power

Battery sampling uses BAT_ADC_PIN=4. BAT_CTRL_PIN=17 keeps the board power latch alive for normal operation and sleep entry.

Audio

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.

SD card

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.

Build the Tetris demo
tools/build-pink-app.sh examples/pink/tetris/tetris.cpp build/pink/tetris.pink

Builds the sample external app.

ABI compatibility

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 state

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.

API surface

External apps include examples/pink/include/pink_app_api.h and use the helpers in examples/pink/include/pink_runtime.cpp.

Pink periodic hooks

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.