Repository navigation
Releases: php-baremetal/php-esp32
Release list
v1.5.0
[1.5.0] - Power Save, Watch Button, Product Protection
Added
secure— Flash Encryption.secure = trueencrypts the flash so a dump can't reveal the baked PHP
source or the.env(they come out as ciphertext). It marks the embeddedstoragepartitionencrypted
(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 = truemakes the chip boot only firmware signed with
your key.phpflash buildprovisions one signing key per unit atdeploys/<MAC>.pem(optional
secure_boot_keys_dir, default./deploys) generated,600
Signature verified successfully. Example:secure-boot.phpflash flashhandles encrypted boards. It detects a chip with Flash Encryption burned and switches
toencrypted-flashon its own, so the one command works before and after a chip is protected — no manual
esptool --encryptor offset juggling.phpflash discoverreports the protection state. It reads the eFuses and printsFlash encryptionand
Secure bootasenabled/not set.power_save— automatic light sleep.power_save = truemakes 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 (defaultI2cBus::SYNCunchanged). Example:
i2c-core1. - MMC5603 magnetometer +
Baremetal\Sensor\Magnetometercapability:mag(),heading(), temperature.
Example:mmc5603. - PCF85063 RTC driver —
now()/set()(BCD at0x51). 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
[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\EventandBaremetal\Events. A typed event facade: a subclass ofEventis 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 returningfalsestops 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-drivenexecution model (type = "event-driven"). Noloop(): 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 alongsideinit-loopandweb-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->deviceis the emitting sensor — so a handler reacts to fresh data
($e->device->drain(), no bus traffic) instead of looping.Baremetal\Sensor\Imu\SamplesReadyis the
shared contract, the same handler for any polled IMU. Example:imu-events. - HTTP as an event source (
[extensions.web]). In theevent-drivenmodel,serve_http(port)
starts an HTTP server whose requests arrive at the reactor asBaremetal\Http\Requestevents; a
listener returns aBaremetal\Http\Response, routed back to the same socket — so a REST API and
hardware reactions run on one resident engine, off the same queue.Routing is userland — the firmware ships no router. Exactly one handler answers (the first to return aEvents::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);
Response); an unmatched path yields404, a handler that throws yields500, and the socket is
never left hanging.Requestexposesmethod,path,query,body,ip,headers, plus
->json()and->header();Responsehasok(),text(),html(),json(),notFound(),
noContent(). The classic superglobals$_GET,$_POST,$_REQUESTand$_SERVERare 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 acrossGET/POST/PUT/PATCH/DELETE/OPTIONS/HEAD. Examples:
http-events(WiFi SoftAP) andhttp-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 aBaremetal\Http\Messageevent with->text,
->client,->binaryand->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 insdkconfig.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
[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/
MEDIUMsize 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) andnow()(call the listeners inline, no
transfer); a dispatch-frame stack that returns every slot a frame still holds when it exits —
normally or through azend_bailoutunwind, 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 withgcc -Werroron
every push and pull request.
[1.0.1] - Explicit pinning - Refactor Base code
[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
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,wifiands3_onboard_rgb: every function now carries typed arginfo (argument and
return types, so reflection and type coercion work properly, includingwifi_scan()'sarray|false
and the nullable?string/?intreturns), and every extension exposes a<name>_available()probe.
Verified on ESP32-S3 hardware (anext-selftestexample exercises the whole
surface). Builds across every PHP version (8.3–8.5). - New
sysextension: 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 formerbaremetal_utility).
Always compiled in.delay()is a plain alias ofsys_delay()(the Arduino idiom); the unprefixed
psram_*/heap_*names are kept as deprecated aliases of thesys_*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 thePHP <ver> on ESP32-S3banner on the serial output -- so a boot regression (PSRAM not
coming up, the engine not starting) fails CI without hardware.ci/run-qemu.shswitches 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 runsqemu-system-xtensa. S3 only (no P4 in QEMU yet).
v0.18.0
[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, likegpio) exposing the ESP-IDF heap:psram_free(),psram_size(),
psram_largest_free(),heap_free(),heap_size(). It fills a real gap -- withUSE_ZEND_ALLOC=0
the Zend heap lives in PSRAM viamalloc, so PHP's ownmemory_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
benchmarkexample: 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. Thesqliteextension 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:sqlite3is about 45 KB smaller (no PDO layer) and gives
theSQLite3/SQLite3Stmt/SQLite3Resultclasses;pdo-sqlitekeeps portable PDO code working.
Wired end to end -- a new enum setting in the manifest (the firstkind = "enum"setting, validated
bycheck-manifest), phpflash's config, interactiveinitand-DPHP_EXT_SQLITE_APIflag, 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. Newsqlite3-notesexample
(the SQLite3-class counterpart tosqlite-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_EXTENSIONdrops
sqlite3_load_extension()from the amalgamation, but bothPdo\Sqlite::loadExtension()and
SQLite3::loadExtension()reference it -- anundefined referencethat 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_VERSIONdefault and a source check firing during
ESP-IDF's early expansion (guarded withCMAKE_BUILD_EARLY_EXPANSION);fetch-php.shnow 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
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 theesp32-p4-wifi-c6board 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), pluswifi_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), pluswifi_ap_ip()/
wifi_ap_clients()/wifi_ap_stop(). Driven straight fromesp_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.envbaked into the
firmware) andwifi-apexamples.- The
web-servermodel 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-serverproject type -- previously reserved for boards with wired Ethernet. The HTTP server was
always independent of the wired-network path; the network simply comes from thewifiextension
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-manageexample: 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']reads0.0.0.0on 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_remotere-exposes theesp_wifi_*API and forwards it
to the companion, so thewifiextension and theweb-servermodel work unchanged. The host
components (esp_wifi_remote+esp_hosted) are pulled automatically for P4 builds (aphpflash builddownloads them viamain/idf_component.yml, gated to theesp32p4target; 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'sname, 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.crenders the table. The same values are also exposed in$_SERVERas
PHP_ESP32_*(deliberately kept out of$_ENVand the process environment).