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/{}")withpathArg(0)andUriGlob("/files/*")(uri/UriBraces.h,uri/UriGlob.h; noUriRegex). - 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. Anhttp.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
HTTPClientkeeps its own response. Two clients that eachGET()before either reads get their own bodies back, andgetString()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, itswifiSsid, 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.
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 outbound TCP.
WiFiClient::connect()returns 0. The type exists becausehttp.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 atecho.mokxi, each message carried as one HTTP request (above), and nothing can connect to its server. The server side works for HTTP: a hand-writtenWiFiServer(the Random Nerd Tutorials web server, on the ESP32 and the ESP8266 alike) gets each Web panel request fromserver.available()as the bytes a browser sends, and what it prints untilstop()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 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, andconfigTime(),NTP.begin()and NTPClient set the same clock ("Ask for the time", above). 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. 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: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.