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_tis Arduino's enum, value for value.HTTPClient's negative codes are Arduino's where Mokxi has the same failure (-1refused,-4not connected,-11timed 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 forgetshandleClient()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.
WiFiClientSecurecompiles and then fails:connect()returns 0 andwhyNot()says TLS is not simulated in Mokxi: there is no https here, only http:// under .mokxi. AnHTTPClientgiven one, or given anyhttps://URL, returnsHTTPC_ERROR_NO_TLSwith 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 becausehttp.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 thewifiRssiproperty says; nothing computes a path loss, and moving the board on the canvas changes nothing. - The clock at
time.mokxiis the kernel's, counted from2026-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 sayssimulatedtoo. Stringis 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:HTTPClientagainstweather.mokxi, the value pulled out of the JSON, and a deliberate request toexample.comso the student reads the refusal in their own serial monitor.wifiapi: a normal GET toapi.mokxi, with its status and JSON printed to Serial. Change the response in Cloud and run the request again.wifilamp:WebServerwith 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.