Simulated WiFi

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.

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.

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 02:4D:4F:4B:58:49.

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

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/getSize/end/errorToString, WebServer::on/onNotFound/ begin/handleClient/send/arg/hasArg/uri/method.
  • 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.

What is not

  • No radio, and so no 802.11 at all. No channel, no scan, no WiFi.scanNetworks(), no beacon, no association frames, no WPA2, no PMK, no encryption anywhere. The password is a string comparison.
  • 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 TCP. WiFiClient::connect() returns 0. The type exists because http.begin(client, url) is the spelling every modern tutorial uses.
  • 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. Two boards in a circuit each get their own private simulated network, and the Web and Cloud panels follow the first one they find.
  • No Bluetooth, anywhere. The classic ESP32, the S3, the C6 and the Pico W all do Bluetooth on real hardware. None of it is modeled and there is no header for a sketch to fail on.
  • 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.
  • 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.

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

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.