Skip to content

Releases: php-baremetal/php-esp32

v1.5.0

Choose a tag to compare

@GianfriAur GianfriAur released this 06 Oct 09:22
829c87c

[1.5.0] - Power Save, Watch Button, Product Protection

Added

  • secure — Flash Encryption. secure = true encrypts the flash so a dump can't reveal the baked PHP
    source or the .env (they come out as ciphertext). It marks the embedded storage partition encrypted
    (data partitions aren't by default) and turns on Flash Encryption (development mode) in the firmware's
    sdkconfig; the .env, already inside the app image, is covered automatically.
    Protect your product.
  • secure_boot — Secure Boot v2. secure_boot = true makes the chip boot only firmware signed with
    your key. phpflash build provisions one signing key per unit at deploys/<MAC>.pem (optional
    secure_boot_keys_dir, default ./deploys) generated, 600
    Signature verified successfully. Example: secure-boot.
  • phpflash flash handles encrypted boards. It detects a chip with Flash Encryption burned and switches
    to encrypted-flash on its own, so the one command works before and after a chip is protected — no manual
    esptool --encrypt or offset juggling.
  • phpflash discover reports the protection state. It reads the eFuses and prints Flash encryption and
    Secure boot as enabled / not set.
  • power_save — automatic light sleep. power_save = true makes the event-driven reactor light-sleep
    between events on its own (RAM retained, instant wake), no sleep code in PHP; power_hold() /
    power_release() bracket a stretch that must stay awake. On a bare S3-Zero idle fell to ~0.95 mA (from
    ~49 mA active). See Power save; example: power-save.
  • I²C bus ownership on core 1. new I2cBus(..., I2cBus::CORE1) runs blocking transactions on the
    core-1 executor over an intent queue, off the PHP core (default I2cBus::SYNC unchanged). Example:
    i2c-core1.
  • MMC5603 magnetometer + Baremetal\Sensor\Magnetometer capability: mag(), heading(), temperature.
    Example: mmc5603.
  • PCF85063 RTC driver — now() / set() (BCD at 0x51). Example: rtc-pcf85063.
  • watch_button() — GPIO button gestures (pressed/released/held/click/repeat/double) with
    debounce and timing in C; button_sampling(false) pauses the sampler before a light sleep. watch_gpio
    stays for the plain falling-edge case.
  • St77916::wake() — re-inits the panel (reset, init, orientation) after light sleep disturbs the QSPI
    lines; call it, then repaint.

v1.4.0: [UPD] rel 1.4.0 Events, Websoket , Http

Choose a tag to compare

@GianfriAur GianfriAur released this 29 Sep 21:21
14ba74f

[1.4.0] - Events and the event-driven model HTTP & Websocket

The reactor arrives: a third execution model where PHP registers listeners and sleeps, and typed
events wake it. Plus the core-1 executor that will feed it. Off by default; a build that doesn't ask
for events pays nothing.

Added

  • Baremetal\Event and Baremetal\Events. A typed event facade: a subclass of Event is an
    event, Events::listen(Class::class, callable) subscribes, Events::now($e) delivers inline and
    Events::dispatch($e) defers (the queue drains breadth-first at the end of the outermost dispatch).
    A listener returning false stops propagation. PHP-originated events are delivered PHP→PHP with no
    C-side buffer — the object is the payload. Registered at startup, always available. Example:
    events-hello.
  • The event-driven execution model (type = "event-driven"). No loop(): the script runs once
    to register listeners and start sources, then a reactor blocks on a cross-core event queue and
    delivers each event to its listeners, sleeping in between — the CPU idles, no busy-wait. Selected
    per build alongside init-loop and web-server. Example:
    event-driven-hello.
  • Timer and GPIO sources. every(ms, Class::class) emits a typed event on a periodic timer;
    watch_gpio(pin, Class::class) emits one on a debounced falling-edge interrupt. Sources are C
    producers that hand a typed event to the reactor across cores.
  • The core-1 executor and I²C polling. A single background task on core 1 samples registered
    pollers at a fixed rate into per-poller ring buffers; $device->poll(hz, depth) starts one and
    sample() / drain() read the latest / accumulated samples. Keeps steady-rate bus reads off the
    PHP core. Example: imu-poll-core1.
  • The executor feeds the reactor. $device->poll(hz, depth, event: SamplesReady::class) makes the
    core-1 poller emit an event on each new sample; it crosses to the reactor on core 0 and is delivered
    as a typed object whose $e->device is the emitting sensor — so a handler reacts to fresh data
    ($e->device->drain(), no bus traffic) instead of looping. Baremetal\Sensor\Imu\SamplesReady is the
    shared contract, the same handler for any polled IMU. Example: imu-events.
  • HTTP as an event source ([extensions.web]). In the event-driven model, serve_http(port)
    starts an HTTP server whose requests arrive at the reactor as Baremetal\Http\Request events; a
    listener returns a Baremetal\Http\Response, routed back to the same socket — so a REST API and
    hardware reactions run on one resident engine, off the same queue.
    Events::listen(Request::class, fn (Request $r): Response => match (true) {
        $r->method === 'GET'  && $r->path === '/status' => Response::json($state),
        $r->method === 'POST' && $r->path === '/echo'   => Response::json($r->json()),
        default => Response::notFound(),
    });
    serve_http(80);
    Routing is userland — the firmware ships no router. Exactly one handler answers (the first to return a
    Response); an unmatched path yields 404, a handler that throws yields 500, and the socket is
    never left hanging. Request exposes method, path, query, body, ip, headers, plus
    ->json() and ->header(); Response has ok(), text(), html(), json(), notFound(),
    noContent(). The classic superglobals $_GET, $_POST, $_REQUEST and $_SERVER are also
    populated per request (a JSON body populates ->json(), not $_POST, as in standard PHP), so existing
    request code works. PHP runs on its own core-0 stack, not httpd's; one request is in flight at a time.
    Verified on hardware across GET/POST/PUT/PATCH/DELETE/OPTIONS/HEAD. Examples:
    http-events (WiFi SoftAP) and http-events-eth
    (wired, a full HTTP test).
  • WebSocket as an event source. serve_ws(path) registers a WebSocket endpoint on the same server;
    each inbound frame arrives at the reactor as a Baremetal\Http\Message event with ->text,
    ->client, ->binary and ->reply($data) to answer that client. HTTP and WebSocket share one
    server and one reactor queue, so a REST API and live push coexist. Requires WebSocket support in the
    HTTP server (CONFIG_HTTPD_WS_SUPPORT, enabled in sdkconfig.defaults). Verified on hardware
    (echo, JSON, per-client frames).
  • ws_broadcast($data). Push a text frame to every connected WebSocket client — the streaming half
    of the model (a sensor window, a state update: nobody asked for it). The live clients are the HTTP
    server's own open sockets, so there is no separate table to keep in sync. Verified on hardware: a 1 Hz
    timer broadcasting a heartbeat reaches multiple concurrent clients at once, alongside request/response
    and echo on the same server.
  • WebSocket backpressure. An inbound frame is handed to the reactor with a blocking enqueue: when the
    event queue is full the HTTP task pauses reading, so TCP flow control slows the sender instead of
    dropping commands. Verified on hardware — blasting 200 frames (far past the queue depth) loses none.

v1.1.0: Event Bus

Choose a tag to compare

@GianfriAur GianfriAur released this 18 Sep 15:39
f797299

[1.1.0] - Event bus core

Groundwork for the upcoming event-driven execution mode: the C core of the event bus. It is not
exposed to PHP yet — an inert foundation with its own CI-gated test suite — so nothing you can call
from a script changes in this release.

Added

  • The event bus C core. An event is a slot from a preallocated pool: fixed TINY / SMALL /
    MEDIUM size classes, free-list based, with no allocation on the hot path. When a class is
    exhausted, acquiring a slot fails instead of blocking or allocating — the pool is the backpressure —
    and a per-tag in-use counter acts as a leak oracle. On top of the pool: dispatch() (queue an
    event; ownership of the slot transfers to the queue) and now() (call the listeners inline, no
    transfer); a dispatch-frame stack that returns every slot a frame still holds when it exits —
    normally or through a zend_bailout unwind, across nested frames — so a handler that throws never
    leaks its slot; two cascade guards (a queue-hop depth that drops the over-limit event, and a
    nested-now() depth that refuses the call before it can overflow the task stack); and a small,
    separate ISR reserve with a drop-and-count policy for events emitted from interrupt context. The
    core is portable C — it compiles and runs on the host, no hardware needed.
  • Event bus test gate in CI. The pool-exhaustion and automatic-release suite — exhaustion per
    size class, an exception mid-dispatch, a cascade that unwinds across frames, an intermediate
    listener that throws (the slot is released exactly once), no leaks over thousands of mixed cycles,
    plus the cascade counters and the ISR reserve — builds and runs on the host with gcc -Werror on
    every push and pull request.

[1.0.1] - Explicit pinning - Refactor Base code

Choose a tag to compare

@GianfriAur GianfriAur released this 13 Sep 16:57
a671a9d

[1.0.1] - Explicit pinning - Refactor Base code
First step of the dual-core work behind the upcoming event-driven mode: make core assignment deterministic instead of leaving it to the scheduler.

Changed
php_task and httpd are pinned to opposite cores. The PHP reactor (php_task) is pinned to core 0 and, in the web-server model, httpd to core 1. Without pinning the scheduler could migrate php_task onto the core the WiFi/lwIP tasks already run on; pinning also lets a request's static-file I/O on httpd overlap with PHP work instead of contending with it, and makes behaviour reproducible for benchmarking. A boot-time log reports the core each task landed on (php_task pinned to core 0, httpd pinned to core 1).
The firmware core is reseamed into boot / task / model runners — a pure code move, no behaviour change, so the coming event-driven mode lands as a clean file instead of a third #ifdef. main.c used to interleave both execution models behind preprocessor branches; it is now split by responsibility: boot.c owns the bootstrap (mounts, network, the embed SAPI hooks, .env, the engine and the project extensions); php_task.c owns the reactor task plus the zend_try/catch + GC wrapper; and each execution model is its own file under main/models/ (init_loop.c, web_server.c), picked at build time by a single model_runner selector. main.c is now just the shared state. The web-server model's former ws_* names are web_, freeing ws_ for real WebSocket support later.

First stable release

Choose a tag to compare

@GianfriAur GianfriAur released this 09 Sep 07:44
f5a47db

The consolidation milestone (see ROADMAP.md); still in progress. What's landed so far:

Added

  • The native extensions refined for a stable 1.0 API. A consistency pass across gpio, sys,
    mem, store, wifi and s3_onboard_rgb: every function now carries typed arginfo (argument and
    return types, so reflection and type coercion work properly, including wifi_scan()'s array|false
    and the nullable ?string/?int returns), and every extension exposes a <name>_available() probe.
    Verified on ESP32-S3 hardware (an ext-selftest example exercises the whole
    surface). Builds across every PHP version (8.3–8.5).
  • New sys extension: system and runtime. Timing (sys_delay(), sys_uptime_ms(),
    sys_micros()), control (sys_restart(), sys_reset_reason()), chip/board identity
    (sys_chip_model(), sys_cpu_freq_mhz(), sys_mac(), sys_idf_version()), and memory
    introspection (sys_psram_free(), sys_heap_free(), …, absorbed from the former baremetal_utility).
    Always compiled in. delay() is a plain alias of sys_delay() (the Arduino idiom); the unprefixed
    psram_* / heap_* names are kept as deprecated aliases of the sys_* memory functions.
  • The test CI is complete. The GitHub Actions pipeline (introduced in 0.18.0) is now finished and
    exercised across the full matrix: manifest checks on every PHP version, and base-firmware builds on
    the default version × every board, every version × one board per family, and two extreme configs
    (a minimal build and a "fat" one with the heavy extensions on). Every build is gated by the per-board
    image-size budget, with the PHP source and ccache cached between runs.
  • QEMU smoke test (gating). The image now boots under Espressif's QEMU, headless, and the job
    asserts the PHP <ver> on ESP32-S3 banner on the serial output -- so a boot regression (PSRAM not
    coming up, the engine not starting) fails CI without hardware. ci/run-qemu.sh switches the S3
    console to UART0 (QEMU doesn't expose USB-Serial-JTAG on -serial), merges the flash from the build's
    flasher_args.json, and runs qemu-system-xtensa. S3 only (no P4 in QEMU yet).

v0.18.0

Choose a tag to compare

@GianfriAur GianfriAur released this 31 Aug 13:18
bf2fc70

[0.18.0] — Benchmarks, memory introspection, a SQLite API choice, PHP 8.5.10, and project foundations

Added

  • baremetal_utility: read the real memory footprint from PHP. A built-in extension (always
    compiled in, like gpio) exposing the ESP-IDF heap: psram_free(), psram_size(),
    psram_largest_free(), heap_free(), heap_size(). It fills a real gap -- with USE_ZEND_ALLOC=0
    the Zend heap lives in PSRAM via malloc, so PHP's own memory_get_usage() reads 0; these
    report the actual heap, which also counts everything (op_arrays, zvals, buffers), not just PHP's
    arena. Registered across every PHP version (8.3-8.5).
  • New benchmark example: real, on-device numbers for memory and GPIO. It
    prints, at boot, the engine's PSRAM baseline, the PSRAM a compiled script costs (three reference
    classes), the working set of a data workload (sized to the PSRAM on hand, so it runs on a 2 MB board
    too), and the GPIO toggle rate from a tight PHP loop. Verified across the PSRAM range -- ESP32-S3-Zero
    "Super Mini" (2 MB Quad), ESP32-S3 (8 MB Octal), ESP32-P4 (32 MB): a compiled script costs ~5x its
    source everywhere (it's the engine, not the chip); the PHP GPIO loop is CPU-bound (~155 kHz at
    160 MHz, ~279 kHz at 360 MHz). Answers the memory and throughput questions people ask with
    measurements instead of estimates.
  • PHP 8.5.10 available. A drop-in patch update of 8.5.9 (all 10 port patches apply unchanged and
    the source file set is identical). Verified on ESP32-S3 hardware. 8.5.9 stays available; the default
    stays 8.4.25.
  • [extensions.sqlite] type: choose the SQLite API layer. The sqlite extension now builds either
    PDO + pdo_sqlite (type = "pdo-sqlite", the default) or the procedural SQLite3-class API
    (type = "sqlite3", ext/sqlite3). Both share the same SQL engine (the amalgamation is built once),
    so the choice is only the API surface: sqlite3 is about 45 KB smaller (no PDO layer) and gives
    the SQLite3/SQLite3Stmt/SQLite3Result classes; pdo-sqlite keeps portable PDO code working.
    Wired end to end -- a new enum setting in the manifest (the first kind = "enum" setting, validated
    by check-manifest), phpflash's config, interactive init and -DPHP_EXT_SQLITE_API flag, and the
    CMake source/registration split across every PHP version (8.3-8.5). Verified on ESP32-S3 hardware:
    both APIs create a table, insert and query. New sqlite3-notes example
    (the SQLite3-class counterpart to sqlite-notes, which now spells out
    type = "pdo-sqlite").
  • Continuous integration (GitHub Actions). Checks each version's manifest and compiles the base
    firmware across versions, boards and project types, with caching and a per-board image-size budget
    (.github/workflows/ci.yml, ci/check-size.sh). QEMU smoke test scaffolded (WIP).
  • Project docs: a roadmap (the 1.0.0 → 3.0.0 milestones), contributing
    guidelines (hardware verification is mandatory before a PR), a security policy, and an
    AI usage policy.

Fixed

  • SQLite loadExtension() no longer breaks the link. SQLITE_OMIT_LOAD_EXTENSION drops
    sqlite3_load_extension() from the amalgamation, but both Pdo\Sqlite::loadExtension() and
    SQLite3::loadExtension() reference it -- an undefined reference that surfaced when SQLite was
    linked alongside other extensions. Added no-op stubs (components/php/compat/sqlite_loadext_stub.c):
    the method links and fails cleanly at runtime, since loading a shared object isn't possible on the chip.
  • CI build reliability. Fixed a stale PHP_VERSION default and a source check firing during
    ESP-IDF's early expansion (guarded with CMAKE_BUILD_EARLY_EXPANSION); fetch-php.sh now caches the
    verified tarball, not the read-only extracted tree, so a restored cache can't leave an empty source dir.

Release v0.17.0

Choose a tag to compare

@GianfriAur GianfriAur released this 27 Aug 20:48
726ed94

Changed

  • Default PHP bumped to 8.4.25 (a drop-in patch update of 8.4.24; all 8 port patches apply
    unchanged and the source file set is identical). 8.4.24 stays available, like every other version.

Added

  • wifi: scan/join a WiFi network and create one, from PHP. An opt-in native extension for
    WiFi-capable SoCs (ESP32 / ESP32-S3 / C-series with a native radio, or an ESP32-P4 with a WiFi
    companion over ESP-HOSTED -- see the esp32-p4-wifi-c6 board below). Client (STA): wifi_scan() lists the nearby APs
    (ssid/bssid/rssi/channel/auth), wifi_connect($ssid, $password = null) joins one (open,
    WPA2, or WPA2/WPA3-transition -- it is PMF-capable, does WPA3-SAE, scans every channel, and
    auto-retries transient association drops so real phone hotspots connect reliably), plus wifi_ip() /
    wifi_rssi() / wifi_connected() / wifi_disconnect(). Access point
    (SoftAP): wifi_ap_start($ssid, $password = null, $channel, $max_conn) makes the board its own
    network with a built-in DHCP server (default 192.168.4.1), plus wifi_ap_ip() /
    wifi_ap_clients() / wifi_ap_stop(). Driven straight from esp_wifi; credentials are never baked
    into the config -- PHP passes them at runtime. Registered across every PHP version (8.3-8.5); ~600 KB
    of flash, so opt-in ([extensions.wifi] enabled = true). Verified on ESP32-S3-Zero hardware (scanned
    real networks, joined a WPA2/WPA3 network and got a DHCP lease, and brought up a SoftAP). New
    wifi-connect (credentials come from a gitignored .env baked into the
    firmware) and wifi-ap examples.
  • The web-server model now runs on WiFi-only ESP32-S3 boards. Every ESP32-S3 has WiFi on the
    die, so all S3 board profiles (esp32-s3-zero, esp32-s3-pico, esp32-s3-eth) now advertise the
    web-server project type -- previously reserved for boards with wired Ethernet. The HTTP server was
    always independent of the wired-network path; the network simply comes from the wifi extension
    instead. A [web-server] init (server_init) script brings WiFi up before the server binds, so a
    board can create its own network and serve pages over it with no router or cable. New
    wifi-ap-s3-rgb-manage example: the board starts a SoftAP and
    serves a live web page (from PHP) that controls the onboard RGB LED's colour and brightness --
    verified end to end on ESP32-S3-Zero hardware (AP up, page served over HTTP on the WiFi, LED reacts
    live to the sliders). ($_SERVER['SERVER_ADDR'] reads 0.0.0.0 on WiFi-only boards -- use
    wifi_ap_ip() / wifi_ip() for the real address.)
  • New board esp32-p4-wifi-c6: WiFi on the radio-less ESP32-P4, via a companion. The P4 has no
    built-in radio, but this board (the ESP32-P4-WIFI6) pairs it with an on-board ESP32-C6
    over SDIO, running ESP-HOSTED: esp_wifi_remote re-exposes the esp_wifi_* API and forwards it
    to the companion, so the wifi extension and the web-server model work unchanged. The host
    components (esp_wifi_remote + esp_hosted) are pulled automatically for P4 builds (a phpflash build downloads them via main/idf_component.yml, gated to the esp32p4 target; git-ignored, like
    the fetched PHP source). Verified on hardware: PHP brought up a SoftAP (wifi_ap_start()), the C6
    answered over SDIO using the ESP32-P4-Function-EV-Board reference pins with no schematic needed
    (already pre-flashed), and the board served a page over its own WiFi. With the P4's 32 MB PSRAM this
    even brings full frameworks over WiFi into reach. See
    docs/reference/special-boards.md.
  • phpinfo() gains a "PHP Baremetal Infos" table. A second table right under phpinfo()'s general
    one, showing the project name (from the config's name, passed by phpflash), the board
    (BOARD_NAME), and the php-esp32 and ESP-IDF versions. The firmware sets these as
    C globals before the engine starts (main.c), and a small per-version patch to
    ext/standard/info.c renders the table. The same values are also exposed in $_SERVER as
    PHP_ESP32_* (deliberately kept out of $_ENV and the process environment).