Reference

Simulated WiFi

The network a sketch joins: what is faithful, what is Mokxi’s own, and what is not there at all.

The contract for Mokxi's network: what a sketch can do, what is faithful, and what is not there at all. Each board's own contract has the register table (docs/esp32c3.md 3.8, docs/esp32c6.md 3.8, docs/esp32.md 3.8, docs/esp32s3.md 3.10, docs/esp8266.md 3.9, docs/picow.md 3.8). crates/net is the model; this file wins over all of them.

The one sentence

The radio is simulated. There is no 802.11, there is no real internet, and the WiFi runtime is Mokxi's own. Nothing in Mokxi transmits, receives, scans, associates, encrypts or opens a socket. A password is compared as a string. The only names that resolve are four made-up ones under .mokxi and the board's own address, and nothing a sketch does can reach the network the browser is on. Every page that mentions WiFi says this, in the properties panel, in the Web and Cloud panels' first line, in the help article, on the board page, and in the examples themselves.

The one exception is opt-in and off on every page load: real internet mode (below), where the page, not the engine, sends a sketch's requests for names outside .mokxi through our gateway, for anyone signed in (a small allowance on Free, more with Pro). With it off, which is the default and the only mode signed out, every sentence above holds exactly. A board that joins Wokwi-GUEST turns it on when a run starts.

Why a mailbox, and why that is honest

On a real ESP32 the WiFi hardware is not a set of documented registers. It is a closed binary blob with a command interface, and every line of Arduino's WiFi.h ends up on the other side of one. So the model is a mailbox: a command register, a status register, a ring buffer each way and an interrupt bit, at 0x6FFF_E000 on the ESP32-C3: Mokxi's own block, not the chip's, above every real peripheral, at an address a real C3 leaves unmapped so that an image using it faults on silicon rather than reading something else's register.

That is the whole of the lie, and it is the same shape as the truth. What it is not is a fake 802.11 MAC: there are no frames, no channels, no rates and no beacon interval anywhere in this code, because a model of those would be an invention with nothing to check it against.

The six boards, and the one number that differs

The block is board-independent. crates/net knows nothing about any board; the SoC chooses one number, the base address, and forwards word accesses. Six boards do, and they are exactly the six that have a radio on the real hardware:

board the mailbox is at what the real part has there
ESP32-C3 DevKitM-1 0x6FFF_E000 nothing: its peripherals stop well below
ESP32-C6-DevKitC-1 0x6FFF_E000 nothing: the same place is empty on this part too
ESP32 DevKit (WROOM-32) 0x3FEF_E000 a reserved gap between the external memory windows and the peripheral window
ESP32-S3-DevKitC-1 0x6FFF_E000 nothing: the peripherals end at 0x600D_0FFF and RTC FAST memory at 0x600F_FFFF
ESP8266 NodeMCU 0x6FFF_E000 nothing: its real blocks are all in the first few kilobytes of that window
Raspberry Pi Pico W 0x4FFF_E000 nothing: above every APB peripheral, below AHB-Lite

On a Pico W the seam is the same shape for a different reason: the wireless part is a CYW43439 on the far side of a three-pin SPI-ish link driven by a closed binary blob, which is no more a set of documented registers on the RP2040 than an ESP32's radio is on its own die. A command interface is what is really there. (The official MicroPython and CircuitPython firmwares for the Pico W drive the chip themselves, and for them it is modeled from its pins instead, with the mailbox unmapped: docs/cyw43.md.)

firmware/include/mokxi_net.h is the one definition of every register, bit and command code; each board's header defines the base address and includes it, and firmware/tools/check_registers.py holds all six headers to their contracts' tables, base address included. A made-up peripheral is the one that most needs the two halves pinned together.

Every other board refuses. A plain Raspberry Pi Pico, the Uno, the Nano, the Mega, the Leonardo, the ATtiny85, the XIAO SAMD21, the Uno R4, the STM32s and the micro:bit have no wireless part on them, so WiFi.h, HTTPClient.h and WebServer.h fail to compile with a sentence pointing here, rather than letting a sketch write to an address its chip does not have. A stub that answered "not connected" would be a lie with a plausible shape; a compile error is not. The Pico and the Pico W therefore carry different program strings (elf32 arm rp2040 and elf32 arm rp2040 picow), as the Uno's and the Nano's do, so the editor offers each board its own examples.

What a student can do

Join the network

The same code on every one of the six boards; the header is WiFi.h on all of them, because that is what the Arduino API is called on the ESP32 family and on the Pico W.

#include <WiFi.h>

WiFi.mode(WIFI_STA);
WiFi.begin("Mokxi-Lab", "breadboard");
while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(250); }
Serial.println(WiFi.localIP());

The SSID and password are properties of the board: click the board in the editor and set wifiSsid, wifiPassword and wifiRssi. All six boards carry the same three, out of crates/parts/src/wifi.rs, with the same defaults and the same validation. With no radio there is nothing to scan and nothing to carry a separate part's settings across, so the one access point a board can see is a setting on the board. The Cloud panel edits the same three values while the sketch runs.

WiFi.status() walks WL_IDLE_STATUS, then WL_DISCONNECTED while it associates, then WL_CONNECTED, which is Arduino's own sequence, because Arduino has no WL_CONNECTING and a part that is associating reports WL_DISCONNECTED. WiFi.localIP() is 192.168.4.2, gatewayIP() is 192.168.4.1, RSSI() is the wifiRssi property, SSID() is the access point's name and macAddress() is the board's own MAC: its wifiMac setting, or, left empty, one the engine works out from the part's id (02:4D:4F: and three bytes), the same on every run and different for each board on the canvas, so ESP-NOW can tell them apart (docs/help/editor/esp-now.md).

Get the password wrong and WiFi.status() becomes WL_CONNECT_FAILED. Get the name wrong and it becomes WL_NO_SSID_AVAIL. WiFi.disconnect() puts it back to idle, and the next begin() leases the same address again.

Fetch something

#include <HTTPClient.h>
WiFiClient client;
HTTPClient http;
http.begin(client, "http://weather.mokxi/");
int code = http.GET();
String body = http.getString();
http.end();

Four origins exist, and no others:

name answers
http://time.mokxi/ the kernel's clock as an ISO string, text/plain
http://weather.mokxi/ JSON with a temperature and a humidity the Cloud panel sets
http://echo.mokxi/ the body you posted, back again; also the simulated WebSocket echo server for WebSocketsClient
http://api.mokxi/ the status, content type and response body configured in the Cloud panel

http://192.168.4.2/ (and http://board.mokxi/) is the sketch's own server, which is the one address a sketch cannot usefully call, because it would have to answer itself; it gets a 404 saying so.

Any other .mokxi name is a 404. Anything that is not under .mokxi does not resolve at all: http.GET() returns HTTPC_ERROR_CONNECTION_REFUSED and HTTPClient::errorToString() returns the one sentence this whole feature is built around: no real internet: names under .mokxi only. https:// returns HTTPC_ERROR_NO_TLS.

Serve something

#include <WebServer.h>
WebServer server(80);

server.on("/", [](){ server.send(200, "text/html", "<h1>hello</h1>"); });
server.begin();
// in loop():
server.handleClient();

on(path, handler), onNotFound(), send(code, type, body), arg(name), hasArg(), uri(), method() and handleClient(). The handlers are your code on the board's core, at the board's clock. The Web panel in the bottom sheet is the browser for them: an address bar at http://192.168.4.2/, a Refresh, and the page rendered in a sandboxed frame with scripts off. Links and form posts inside it are caught and routed back into the simulated network, so "switch the LED from a web page" works end to end.

Nothing outside the browser tab can reach that server. There is no listening socket. The Web panel is the only client that exists.

Watch what happened

The Cloud panel is the network tab: every request the board made and every request the page made into the board, with the method, the URL, the status, the bodies and, when a request failed, a line saying why. It also carries the access point's three settings and the two weather.mokxi values. It also has an API lab: choose the HTTP status, content type and response body for api.mokxi, then watch the next request bring that exact response back. A teacher can model success, bad input, missing data and server errors without an API key or a dependency outside the lesson.

Use Blynk

#define BLYNK_TEMPLATE_ID "TMPLxxxxxx"
#define BLYNK_TEMPLATE_NAME "Light Intensity Monitor"
#define BLYNK_PRINT Serial
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>   // BlynkSimpleEsp32.h, or BlynkSimpleWifi.h on a Pico W or UNO R4 WiFi

BlynkTimer timer;
BLYNK_WRITE(V1) { digitalWrite(D1, param.asInt()); }
void sendLight() { Blynk.virtualWrite(V0, analogRead(A0)); }
void setup() {
  Blynk.begin("token", "Mokxi-Lab", "breadboard");
  timer.setInterval(1000L, sendLight);
}
void loop() { Blynk.run(); timer.run(); }

Real Blynk keeps a TCP connection to blynk.cloud, which this network does not have, so the Blynk server is simulated (crates/net/src/blynk.rs), and the library is Mokxi's own (firmware/lib/mokxi_blynk.h, the Blynk IoT library's calls, v1.3). The sketch reaches the server through one mailbox command, BLYNK, answered at once: there is no wire format and no latency to model, because there is no wire. The Cloud panel's Blynk section, drawn once a sketch logs in, is its console: each virtual pin the sketch wrote or has a BLYNK_WRITE for, its value, who wrote it and when, and a box that sends a value to it, as the Blynk app's switch or slider would. Blynk.logEvent's events are listed under it.

What follows the library: Blynk.begin() blocks until the board is on the network and logged in; BLYNK_WRITE runs inside Blynk.run(), never between two lines of loop(); BLYNK_CONNECTED runs on each login; Blynk.syncVirtual() and syncAll() send the server's stored values back into BLYNK_WRITE; a write while the board is offline is dropped, and a value sent from the panel while it is offline is stored but not delivered; a float goes out with three decimals and a double with seven; a board that leaves the network loses its server, and Blynk.run() logs in again five seconds after it is back. The datastreams survive a board reset, as they do on a real server. BlynkTimer is the library's: sixteen timers from timer.run(). WidgetLED, WidgetTerminal and WidgetLCD write their pins as the library does (WidgetLCD.h and the other widget headers old sketches include by name are there too); the LCD is not drawn, its pin shows the last clr or p, x, y, text.

What does not: the token, template ID and server name are shown, never checked or looked up (there is no Blynk account here); BLYNK_APP_CONNECTED compiles and never runs, since nothing opens an app (BLYNK_READ is gone from Blynk 1.4.0, so a sketch that uses it gets a build error naming Blynk.virtualWrite() from a BlynkTimer instead); Blynk.Edgent, OTA and the _SSL headers are not here. Values are capped at 1024 bytes, 64 writes wait for a board that is not polling, and the panel keeps the last 20 events. Blynk's traffic is not in the request log, which it would fill.

Use WebSockets (the echo server)

#include <WiFi.h>
#include <WebSocketsClient.h>   // the WebSockets library, Markus Sattler

WebSocketsClient webSocket;
void webSocketEvent(WStype_t type, uint8_t *payload, size_t length) {
  if (type == WStype_CONNECTED) webSocket.sendTXT("Connected");
  if (type == WStype_TEXT) Serial.printf("[WSc] get text: %s\n", payload);
}
void setup() {
  // join Mokxi-Lab first
  webSocket.begin("echo.mokxi", 80, "/");
  webSocket.onEvent(webSocketEvent);
}
void loop() { webSocket.loop(); }

This is not a WebSocket on a wire. A WebSocket is a TCP connection that outlives its opening HTTP request, and this network has no TCP. So the library (firmware/lib/WebSocketsClient.h, the calls of 2.6.1) gives echo.mokxi one more job: it is the simulated echo server. The first loop() after begin() makes an HTTP GET to http://echo.mokxi<url>, and its answer raises WStype_CONNECTED. Each sendTXT() is then an HTTP POST of the text to that URL, and what the echo site sends back is handed to the callback from the next loop() as WStype_TEXT, in the order sent; sendBIN() travels as base64 text and comes back as WStype_BIN, decoded; sendPing() comes back as WStype_PONG, so enableHeartbeat() keeps the connection. Every message is a request in the Cloud panel's log, because that is all it is. A send blocks for the request's 60 ms, where upstream's returns once the frame is buffered.

Any other host, and beginSSL() to any host, never connects and makes no request: loop() raises WStype_DISCONNECTED, tries again every setReconnectInterval(), and the serial monitor says once that WebSockets to that host need a real board. Real internet mode does not change that: the gateway carries one HTTP request and one answer. WebSocketsServer builds and runs, and no client ever connects to it, since the Web panel shows pages with scripts off; it says so once too.

Ask for the time

#include <WiFiUdp.h>
#include <NTPClient.h>
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 3600, 60000);
// after WiFi.begin() has connected:
timeClient.begin();
timeClient.update();
Serial.println(timeClient.getFormattedTime());   // "01:00:03"

This is not the real current time. NTP is a UDP exchange with a server on the internet, and this network carries no UDP and reaches no server; the simulation does not know today's date either, and inventing one would be a lie a student could not check. So NTP is simulated, the same way on every WiFi board: configTime() on the ESP32 and ESP8266 boards, NTP.begin() on the Pico W, and NTPClient (firmware/lib/NTPClient.h, the arduino-libraries release 3.2.1) all set the clock to the simulation's own: 2026-01-01 00:00:00 UTC (epoch 1767225600) at power-up, plus the seconds the board has run, which is also what http://time.mokxi/ reports. Each says so once on the serial monitor. NTPClient's getEpochTime() is that, plus its offset; update() asks when the interval has passed and forceUpdate() asks now, and both are answered while the board is on the network and time out after about a second when it is not. No packet is sent: the WiFiUDP it is given is not used.

Use wired Ethernet (ETH.h, the ESP32 boards)

#include <ETH.h>
Network.onEvent(onEvent);   // ARDUINO_EVENT_ETH_START, _CONNECTED, _GOT_IP
ETH.begin();                // or the core 2.x / 3.x RMII and W5500 forms

There is no PHY, no RMII, no W5500 and no cable. ETH.begin() is the mailbox's NET_CMD_ETH (operation 1): the station state machine joins with no SSID and no password, WIRED_PS (1.5 s) later, with the same lease, and everything that rides on the station (HTTP, the sketch's server, Blynk, the Real internet gate and its limits) rides on this unchanged. There is one link, so WiFi.status() reads it whichever way it came up, and WiFi.begin() moves it back to WiFi. While the link is the wired one, WiFi.onEvent() and Network.onEvent() handlers get the core's ETH events instead of the station's. ETH.config() is kept for localIP() to report; the board still answers the Web panel at 192.168.4.2. The cable is always plugged in.

What is faithful

  • The API. The calls and their signatures are the ones a student copies out of an ESP32 tutorial: WiFi.begin/status/localIP/gatewayIP/RSSI/SSID/ macAddress/disconnect/mode, WiFiClient, HTTPClient::begin/GET/POST/ addHeader/getString/getStream/getSize/end/errorToString, WebServer::on/onNotFound/ begin/handleClient/send/arg/hasArg/uri/method, and routes with patterns in them: UriBraces("/toggle/{}") with pathArg(0) and UriGlob("/files/*") (uri/UriBraces.h, uri/UriGlob.h; no UriRegex).
  • The status codes. wl_status_t is Arduino's enum, value for value. HTTPClient's negative codes are Arduino's where Mokxi has the same failure (-1 refused, -4 not connected, -11 timed out).
  • The timing envelope. An association takes between one and three seconds, and a wrong password fails after five. Both are inside what a real ESP32-C3 on a quiet network does, and both are held there by a test.
  • The order of the states, which is what a connect loop is written against.
  • The cost. A blocked http.GET() really costs the sketch its 60 ms of simulated time, because the rest of the circuit runs through it. A sketch that forgets handleClient() really never answers. An http.setTimeout() shorter than that times out (-11), and the request it gave up on is dropped and logged when the next one starts, as a real client closes the connection it stopped waiting on; the next request is not refused.
  • Each HTTPClient keeps its own response. Two clients that each GET() before either reads get their own bodies back, and getString() twice gives the body twice, as the ESP32 core's does.

What is not

  • No radio, and so no 802.11 at all. No channel, no real scan, no beacon, no association frames, no WPA2, no PMK, no encryption anywhere. The password is a string comparison. WiFi.scanNetworks() compiles and "finds" the one access point the board can see, its wifiSsid, and says on the serial monitor that the scan is simulated (firmware/include/WiFi.h).
  • No real internet. No DNS protocol, no TCP, no IP stack, no sockets. The four origins are Rust functions. No request in Mokxi ever leaves the tab, and the Web panel's frame is served with default-src 'none' so it cannot fetch an image from outside either.
  • No TLS. WiFiClientSecure compiles and then fails: connect() returns 0 and whyNot() says TLS is not simulated in Mokxi: there is no https here, only http:// under .mokxi. An HTTPClient given one, or given any https:// URL, returns HTTPC_ERROR_NO_TLS with the same sentence. A TLS that trusted everything would teach a student that https is free.
  • No raw outbound TCP. WiFiClient::connect() returns 0. The type exists because http.begin(client, url) is the spelling every modern tutorial uses. So no WebSocket either: the WebSockets library's client reaches only the simulated echo server at echo.mokxi, each message carried as one HTTP request (above), and nothing can connect to its server. The server side works for HTTP: a hand-written WiFiServer (the Random Nerd Tutorials web server, on the ESP32 and the ESP8266 alike) gets each Web panel request from server.available() as the bytes a browser sends, and what it prints until stop() is read back as the HTTP response the panel shows.
  • No soft AP. WiFi.mode(WIFI_AP) is accepted and does nothing; WiFi.modeNote() says only station mode is simulated.
  • No second board on the network. One board, one lease, one access point. (ESP-NOW is the exception that is not a network: frames go board to board over the air every ESP board on the canvas shares, docs/help/editor/esp-now.md.) Two boards in a circuit each get their own private simulated network, and the Web and Cloud panels follow the first one they find.
  • Bluetooth is a separate simulation with the same honesty (docs/bluetooth.md): the ESP32 boards' Bluetooth Classic and LE talk to a phone in the editor's Bluetooth pane, through registers of their own in this same block. The Pico W's Bluetooth is not modeled.
  • No interrupt on four of the six boards. On the classic ESP32, the ESP32-S3, the ESP8266 and the Pico W the mailbox raises no CPU interrupt at all: an interrupt matrix source number and an NVIC IRQ number are the chip's, and this block is not the chip's, so there is no honest number to give it. The runtime polls, and a parked core is still woken when the network has something to say.
  • The numbers are settings, not measurements. RSSI() is whatever the wifiRssi property says; nothing computes a path loss, and moving the board on the canvas changes nothing.
  • The clock at time.mokxi is the kernel's, counted from 2026-01-01T00:00:00Z, not the time of day. The simulation does not know what day it is and inventing one would be a lie a student could not check. The body says simulated too, and configTime(), NTP.begin() and NTPClient set the same clock ("Ask for the time", above).
  • String is Arduino's, from the shared core (firmware/lib/WString.h), with its text on the heap. Bodies are still capped by the sizes below.
  • Sizes are capped. Each mailbox ring is 4096 bytes, a request body is at most 2048 and a response 4096; the Cloud panel keeps 512 bytes of each body and the last 100 requests. A page the sketch's server sends shares its ring with its content type, so it is cut a few bytes short of 4096, and when it is cut the Web and Cloud panels both say so.

The page's end

The panels reach the network through Component::ask, the kernel's third generic channel beside poke and debug: an opaque JSON string down, an opaque JSON string back, and a kernel that reads neither. The operations are state, log, setAp, setWeather, setApi, blynkWrite, clearLog, fetch and result; state and log both answer with "simulated": true in them, so a panel cannot render without it. crates/net/src/lib.rs is the list.

Real internet mode

An opt-in mode (worker/pro-pack.ts): the Cloud panel's Real internet switch, which turns on only after GET /api/gateway says the account may, and answers which caps apply. Free, signed in with a confirmed email address, gets FREE_GATEWAY; the Pro pack (Pro, Student, Teacher, Classroom, a paid class, the Pro trial) gets GATEWAY; a class student or a child under 13 only where a teacher whose class tools are on switched it on for their class (classes.real_internet, default off), with GATEWAY; signed out and suspended accounts, never. A board whose wifiSsid is Wokwi-GUEST, the access point Wokwi gives a board and a Wokwi import names, turns the switch on by itself when a run starts (autoFor), because on Wokwi that access point reaches the internet; turned off by hand, it stays off for the session.

In the engine (crates/net): setReal {on} sets the mode. While it is on, a CMD_REQUEST whose URL routes out (http::routes_out: any host outside .mokxi, https:// included, and mqtt.mokxi) is not served after HTTP_LATENCY_PS. It waits, with no clock of its own, for the page: outbound answers the one waiting request (id, method, url, contentType, body), and outboundResult {id, status, contentType, body, note} finishes it through the same path a simulated answer takes (ring, log, CMD_POLL). Status 0 is a refusal and reaches the sketch as HTTPC_ERROR_CONNECTION_REFUSED. A sketch that gives up (its own HTTPClient timeout, five seconds by default) and asks again drops the old request rather than getting BUSY, and a late answer for it finds nobody waiting. The simulated origins and the board's own address never route out.

In the page (web/src/ui/real-internet.ts): a pump asks outbound every 80 ms, sends what it finds to POST /api/gateway, and hands the answer back.

In the Worker (worker/gateway.ts): one request in, one answer out. GET and POST only, http and https on ports 80, 443, 8080 and 8443, no credentials in the URL; hosts refused by IP range (private, loopback, link-local, CGNAT, documentation, benchmarking, multicast, reserved; IPv6 outside global unicast, and mapped or NAT64 addresses judged by their IPv4 part), by name (one label, localhost, .local, .internal and the other local suffixes, Cloudflare's platform names, mokxi.com), and again by what the name resolves to over DNS over HTTPS, and the owner's denylist (GATEWAY_DENY_HOSTS, a public var: each name refuses itself and every name under it, brokers included). Redirects are never followed. Only the sketch's content type goes out as a header. Bodies are capped: on Pro at the mailbox's own sizes, 2048 bytes out and 4096 back, on Free half that; a non-text body is left out. The host gets four seconds. Per account and per project: a minute's burst in memory (Pro 30 and 20, Free 10 and 10) and a UTC day's total in gateway_counts (Pro 1000 and 500, Free 100 and 50). Free is also held per address (FREE_GATEWAY_PER_IP: 20 a minute in memory, 300 a day), so a stack of throwaway accounts on one connection does not add up; Pro is not, so a classroom behind one address keeps working. That table is the only thing written: counts per day per scope, the address's scope a salted hash that changes every day, never a URL, host, body or address.

MQTT: POST http://mqtt.mokxi/publish?broker=<name>&topic=<topic> with the message as the body. The gateway publishes it at QoS 0 over MQTT over WebSockets (worker/mqtt.ts) to one of test.mosquitto.org, broker.hivemq.com or broker.emqx.io; a Worker cannot open a TCP socket to port 1883, so PubSubClient and raw TCP still do not work, and nothing is subscribed to or brought back. Wildcard topics are refused; a message is at most 1024 bytes on Pro and 512 on Free.

WiFiClientSecure is still refused by the runtime; http.begin("https://...") and a plain WiFiClient reach the gateway, which does the TLS.

Telegram (UniversalTelegramBot.h, Brian Lough's library, 1.3.0) is the one library built on that path. Telegram is a real service, so a bot reaches it only in this mode: each call (getMe, getUpdates?offset=...&limit=1, sendMessage and the rest as a POST of JSON) is one HTTPClient request to https://api.telegram.org/bot<token>/<method>, which the gateway sends and answers. The WiFiClientSecure a sketch hands the bot, and the TELEGRAM_CERTIFICATE_ROOT it gives that client, carry nothing: the gateway checks Telegram's certificate. With real internet off, every call fails at once (false, 0 or "") and the serial monitor says once: Telegram needs the real internet: switch on Real internet in the Cloud panel (signed in), or run this on a real board. A refusal, a 429 from the caps above, a wrong token and an answer cut at the size cap each say what they were, once. The caps shape a bot: the upstream EchoBot polls every second, which is twice Pro's minute and six times Free's, so Mokxi's copy of it polls every ten seconds, and on Free a project's 50 a day last about eight minutes of that. longPoll is held to three seconds, since the host gets four. Photos and files sent as bytes are not carried (the gateway takes text), and on the ESP8266, whose code runs from 32 KB of instruction RAM here, the EchoBot fits with under a kilobyte to spare, so the library reads Telegram's answers with a small reader of its own rather than an ArduinoJson document.

The examples

  • wificonnect: the connect loop, the address, the signal, and the wrong password on purpose.
  • wifiweather: HTTPClient against weather.mokxi, the value pulled out of the JSON, and a deliberate request to example.com so the student reads the refusal in their own serial monitor.
  • wifiapi: a normal GET to api.mokxi, with its status and JSON printed to Serial. Change the response in Cloud and run the request again.
  • wifilamp: WebServer with two links and a form, switching the LED on GPIO 8 from the Web panel.

wificonnect, wifiweather and wifilamp ship for all six boards and wifiapi for the ESP32-C3, the ESP32-C6, the ESP32-S3 and the Pico W, twenty-two sketches, each driving the pin its own board's blink example drives, so the circuit that watches one watches the other. On the Pico W that is LED_BUILTIN and GP15, because the on-board LED sits on the CYW43439 and is not on the header. On the ESP32-S3 it is the on-board RGB LED, lit white, and GPIO 2.

Every WiFi board compiles them in the browser: the headers are in the runtime bundles, rebuilt with node web/scripts/bundle-runtime.mjs. The classic ESP32, the ESP32-S3 and the ESP8266 build on clang-v3, the wasm build of Espressif's LLVM fork, since 2026-09-28. Their untouched examples still run the prebuilt ELFs Espressif's GCC builds (deploy/fetch-xtensa.sh); docs/compile.md says why.