HalfSweet/PocketJS-IDF

PocketJS runtime and RGB565 framebuffer manager for ESP32-P4

★ 9Forks 1CGitHub ↗Compare

README

PocketJS-IDF

简体中文

PocketJS runtime and RGB565 logical framebuffer management as one reusable ESP-IDF component for ESP32-P4.

The component owns:

  • one PocketJS/QuickJS runtime and one .pocket application;
  • one RGB565 logical display;
  • dirty-region history, one-pixel damage halo, U8.4 phase alignment, chunking, caller-provided strip reuse, and flush transactions;
  • PPA FILL, A8 BLEND, PSM5650 SRM, and ordered software fallbacks;
  • advanced QuickJS extensions for board features such as an IMU.

The board project continues to own the panel, touch, sensors, rotation, scaling, physical framebuffers, DMA, and swap/retirement policy.

Compatibility

Item v0.1 contract
SoC ESP32-P4 only
ESP-IDF >=5.4; CI: 5.4.4 and 6.0.2
QuickJS espressif/quickjs-ng 0.14.0
Rust target riscv32imafc-unknown-none-elf
Raster format RGB565
Runtime/display one application, one runtime, one attached display
Input button bitmap and signed analog X/Y

Clone for development

PocketJS framework, compiler, Core, and ESP32-P4 renderer source is an official Git submodule fixed to the reviewed upstream commit a4e154789655cafa9dc0f57a8c83fc2114d74776:

git clone --recurse-submodules \
  https://github.com/HalfSweet/PocketJS-IDF.git
cd PocketJS-IDF
git submodule update --init --recursive

The submodule URL is https://github.com/pocket-stack/pocketjs.git. The renderer changes were accepted through pocket-stack/pocketjs#190. Component packing flattens the required source files into the Registry archive, so Registry consumers do not need Git or submodule initialization.

Add the component

Add this to the consumer component's idf_component.yml:

dependencies:
  halfsweet/pocketjs-idf:
    version: "^0.1"

For local development, temporarily add:

    override_path: "/absolute/path/to/pocketjs-idf"

The final directory must be named pocketjs-idf or halfsweet__pocketjs-idf, as required by ESP-IDF dependency resolution.

Install the Rust target once:

rustup target add riscv32imafc-unknown-none-elf

The normal ESP-IDF build requires Cargo. It never installs host tools automatically.

Embed an application

Public runtime input is an official .pocket package for target esp32p4-idf, host ABI 1, platform esp-idf.

Prebuilt package (no Bun required)

idf_component_register(SRCS "main.c")

pocketjs_embed_package(
    TARGET ${COMPONENT_LIB}
    NAME dashboard
    PACKAGE "app/dashboard.pocket"
)

This validates the package and generates pocketjs_app_dashboard.h plus the pocketjs_app_dashboard descriptor in the ESP-IDF build directory.

Compile application source

First install the pinned compiler dependencies:

bun install --cwd managed_components/halfsweet__pocketjs-idf/vendor/pocketjs \
  --frozen-lockfile

Then use:

pocketjs_compile_app(
    TARGET ${COMPONENT_LIB}
    NAME dashboard
    MANIFEST "app/pocket.json"
)

The helper compiles a deterministic esp32p4-idf ABI 1 .pocket, writes a depfile, and generates the same C descriptor. All outputs stay under the ESP-IDF build directory. Missing Bun or dependencies produce an actionable error and are never installed implicitly.

Run headless

#include "pocketjs.h"
#include "pocketjs_app_dashboard.h"

const pocketjs_config_t config = POCKETJS_DEFAULT_CONFIG();
pocketjs_t *runtime = NULL;
ESP_ERROR_CHECK(pocketjs_create(
    &pocketjs_app_dashboard,
    &config,
    &runtime
));

pocketjs_frame_stats_t stats = {0};
ESP_ERROR_CHECK(pocketjs_run_frame(runtime, NULL, &stats));
pocketjs_destroy(runtime);

The package bytes must remain readable until pocketjs_destroy() returns. The caller chooses the task, core affinity, frame rate, and watchdog delay.

Attach a display

Create a logical RGB565 display object, supply one or two caller-owned 128-byte-aligned draw buffers, and register a mandatory flush callback:

pocketjs_display_t *display = NULL;
ESP_ERROR_CHECK(pocketjs_display_create(680, 360, &display));
ESP_ERROR_CHECK(pocketjs_display_set_buffers(
    display,
    strip_a,
    strip_b,
    sizeof(strip_a),
    POCKETJS_DISPLAY_RENDER_MODE_PARTIAL
));
ESP_ERROR_CHECK(pocketjs_display_set_callbacks(
    display,
    &callbacks,
    board,
    1000
));
ESP_ERROR_CHECK(pocketjs_attach_display(runtime, display));

flush receives a half-open logical area, immutable RGB565 pixels, stride, stable target ID, and is_last. A synchronous driver calls pocketjs_display_flush_ready() before returning. An asynchronous driver later calls that function from the owner task or pocketjs_display_flush_ready_from_isr() from an ISR.

The modes are:

  • PARTIAL: one or two buffers of at least one complete logical row;
  • DIRECT: one or two complete logical framebuffers, updated in place using stable target-specific damage history;
  • FULL: one or two complete framebuffers, redrawn and submitted every frame.

Optional begin_frame, end_frame, and abort_frame callbacks expose stable front/back target IDs and phase-alignment requirements for physical double-buffer drivers. Rotation, scaling, physical addresses, and DPI swaps remain entirely inside those callbacks.

Except for pocketjs_display_flush_ready_from_isr(), all APIs are non-reentrant and must be called from one owner task on one core.

QuickJS extensions

Include pocketjs_quickjs.h only for version-pinned native extensions. Extensions can install globals, update them in before_frame, and release state in destroy. JSContext * is valid only during those callbacks. Pending exceptions and rejected promises make the current frame fail, are logged and cleared, and do not permanently disable later frames.

The Tab5 BMI270 bridge belongs in this layer; it is intentionally not part of the generic runtime.

Examples and documentation

Both examples execute one headless frame. Display callbacks remain board-specific and are exercised by the Tab5 consumer repository.

Contributors

HalfSweet

Issues