5.6 Networking¶
A real ZX Spectrum Next has a header for an ESP-01 WiFi module, and the
software written for it — NXtel, nextsync, the NextZXOS .HTTP and .zxdb-dl
dot commands — talks to that module over a serial link. JNEXT emulates one, so
those programs can reach the network through your host machine.
There is no WiFi to configure. The emulated module reports itself as already associated with an access point, because to the guest program that is what a working ESP-01 looks like; the actual connections are made by your host.
It is off by default, and it stays off until you say otherwise¶
Turning this on gives the running program an outbound TCP and UDP pipe out of the emulator. That program can already read and write the SD-card image. A program you did not write is a program you are trusting.
So JNEXT never enables it for you, and no loaded program can enable it for itself. You turn it on in one of two places:
- for one run,
--espon the command line; - permanently, Settings > Preferences > Network.
--no-esp forces it off for a single run, whatever the saved setting says.
That is the escape hatch that makes the permanent setting safe to have: if you
have the ESP on by default and want to run something you do not trust, start it
with --no-esp.
The setting is stored in ~/.jnext/jnext.conf under [esp], and it is the one
Preferences setting that cannot be applied to a machine that is already
running — the module is built when a machine starts. Change it and it takes
effect on the next Machine > Power Reset (or F1), or the next launch. You
do not have to quit JNEXT.
You can always see whether it is on¶
Whenever the module is enabled, the status bar grows an ESP cell. However
you turned it on — command line, config file or Preferences — the cell is
there. (In --headless runs there is no window and so no status bar; the
startup log line is the equivalent, and 7.1
covers that mode.)
Idle, it names the restriction in force, so you can tell at a glance whether your allowlist actually loaded:
ESP: on (any host) no restriction on the host name
ESP: on (2 allowed) two named hosts, listed in the tooltip
Once the program starts connecting, the cell follows it:
| Cell | Means |
|---|---|
ESP: host:port |
connected |
ESP: closed host:port |
that connection has ended |
ESP: failed host:port |
the attempt did not succeed — see below |
ESP: REFUSED host:port |
red. JNEXT blocked it: either your --esp-allow list does not name that host, or the address is on the always-refused list below |
ESP: FAULT |
red. The host side broke; the ESP has stopped making progress |
Red REFUSED means JNEXT said no, and it covers both halves of that
decision. failed means the network said no: the name did not resolve, the
remote end refused the connection, or the attempt timed out. Hover the cell
either way — the tooltip carries the reason, and a block by the always-refused
list says so in as many words (address refused by policy: …). The log also
carries it, as a warning, at the default log level.
Hover the cell for the full history of the session — the cell can only show the most recent event, and "it flashed something a minute ago" needs to stay answerable.
A worked example: NXtel¶
NXtel is a Viewdata/teletext client for the Next, and it is the program this has been verified against end to end, over the live internet.
It ships on the NextZXOS SD-card image, and it has to be launched from
NextZXOS — loading NXtel.nex directly with --load stops at an error about
its NXTEL.CFG file, and that is not an ESP problem (it does the same with the
ESP off).
Start JNEXT with the ESP on, restricted to the BBS:
jnext --esp --esp-allow nx.nxtel.org
Then, in the emulated machine: Browser from the NextZXOS menu, into
APPS, then WIFI, then NXtel, and run NXtel.nex. Choose Connect, and
pick the NXtel (Wendy) entry.

Connected to WENDY and a green ONLINE mean the whole chain worked: the
program, the emulated module, your host's network, and the real server.
Two things surprise people the first time:
- NXtel writes a
NXtel.cfgfile next to itself on first run and opens it in an editor. That looks like a failure and is not — it is the config being created. Carry on. - The SD-card image is written to. If you need the run to be reproducible, give it a copy of the image rather than your master.
Restricting which hosts a program may reach¶
--esp-allow HOST narrows the program to hosts you name. Give the option once
per host — it is not a list:
jnext --esp --esp-allow nx.nxtel.org --esp-allow 192.168.1.10
A comma is rejected rather than accepted, because --esp-allow a.com,b.com
would otherwise become a single host name that nothing ever matches: every
connection refused, while the allowlist looks like it names two hosts.
--esp-allow without --esp is likewise an error rather than a silent no-op,
so you cannot end up believing a restriction is in force when the module is not
even on.
In Preferences > Network the same list is one host per line (a comma separates there too, since that is how the config file writes it). Blank lines and repeats are dropped. The box is greyed out while the module is off, because on its own it restricts nothing.
Matching is exact and case-insensitive. There are no wildcards, and an IP address must be listed as itself — a deliberately small rule for a security control, and enough for the programs that exist: they connect to a handful of literal names from their own config.
With no --esp-allow at all, the program may name any host. An empty list
means "no restriction", not "deny everything" — otherwise --esp on its own
would do nothing and the module would just look broken. The startup log and the
status cell both say which of the two you are in.
What is refused no matter what you allow¶
These are judged by the address a name actually resolves to, before any
connection is attempted, and --esp-allow cannot re-enable them:
- loopback —
127.0.0.0/8and::1: daemons on your own machine; - link-local —
169.254.0.0/16andfe80::/10; - cloud metadata endpoints —
169.254.169.254,100.100.100.200andfd00:ec2::254; - the unspecified addresses
0.0.0.0/8and::; - multicast and reserved space —
224.0.0.0/4,240.0.0.0/4,255.255.255.255,ff00::/8.
IPv6 spellings of an IPv4 address (::ffff:127.0.0.1, NAT64, 6to4) are resolved
to what they really reach and judged on that, so they cannot be used to slip
past the list.
Your own LAN is reachable on purpose. Private addresses (192.168.x.x,
10.x.x.x, 172.16-31.x.x, and likewise carrier-grade NAT 100.64.0.0/10 and
IPv6 unique-local fc00::/7) are not refused — reaching another machine on
your own network is the main thing people use this for, and nextsync in
particular connects to a PC on the LAN. The two cloud-metadata addresses above
sit inside those ranges and are carved back out.
Worked example: NextSync¶
NextSync is the file-transfer tool most Next developers use to get a build onto
the machine, and it is the one case where the loopback rule above changes what
you have to type. Its server runs on your PC; the .sync dot command on the Next
connects out to it.
Run the server bound to your machine's LAN address, not 127.0.0.1. The
server script binds every interface already, so this is only about which address
you give the Next:
python3 nextsync.py -a
Then, once, on the Next — with your host's real LAN address, which the server prints when it starts:
.sync 192.168.100.238
and from then on just .sync (or .syncfast / .syncslow). Nothing else is
needed: --esp alone is enough, because private addresses are allowed by default
and an empty allowlist means "no restriction".
jnext --esp --machine next
Pointing it at 127.0.0.1 is the one thing that will not work — the connection
is refused before it is attempted, and the log says
address refused by policy. All three speed variants are verified working, and
the files arrive byte-identical.
Nothing about your host leaks into the program¶
The module reports a fixed, invented identity: SSID JNextWifiHost, a
02:00:00:… MAC and BSSID, and a 192.168.1.50 station address. None of it is
read from your machine, so a program cannot learn your real SSID, MAC or local
addresses by asking. The emulated module is not a radio and never scans.
--esp-ip-address replaces the station address it claims, and does not weaken
this: the value is one you typed, never one jnext read off your machine.
When something does not work¶
Turn up the module's own log and watch what the program is actually saying:
jnext --esp --log-level esp01=debug
| Level | What you get |
|---|---|
info (default) |
the module coming up, and every connection opened, refused, failed or closed |
debug |
every AT command the program sends and every reply |
trace |
the raw bytes in both directions |
--log-level esp01=off silences the connection reports as well. That is your
call to make and JNEXT will not override it — in a window the status-bar cell
stays regardless, so the run is still not silent there. Headless, with the log
off, nothing reports the connections at all.
Common cases:
- The cell shows red
REFUSED. JNEXT blocked it. Hover it: if the reason isnot in the allowlist, check your--esp-allowspelling against the name the log shows the program asking for, or drop the restriction to confirm that is what it is. If it isaddress refused by policy, the program asked for something on the always-refused list and no--esp-allowwill let it through. - The cell shows
failed. That is an ordinary network failure — wrong name, nothing listening, timeout. Hover it for the reason. - The program behaves as if nothing is there. In a window, check the cell
exists at all; headless, check the startup log for the
ESP-01 enabledline. If it is absent the ESP is off —--no-espbeats a saved preference, and a changed[esp]setting only takes effect after a Power Reset. - Nothing at all happens and the program hangs. Run it once with
--no-esp. If it behaves the same way, the problem is not the networking.
Letting a program listen¶
A program can also accept connections instead of only making them: it sends
AT+CIPMUX=1, then AT+CIPSERVER=1,<port>, and jnext listens on that port and
hands it each connection that arrives. A debug stub running on the emulated Next
needs exactly this, because a debugger on your PC dials out and never listens.
Everything above is about which addresses a program may dial. A listening port points the other way — it exposes the program to whatever can reach the port — so the allowed-host list has nothing to say about it, and the boundary is the bind address instead:
- by default the port is bound to
127.0.0.1, so only this machine can reach it. That is all the intended use needs, and it needs no flag; --esp-listen-address ADDRwidens it.--esp-listen-address 0.0.0.0means any machine that can reach yours can talk to the running program, with no authentication in front of it. That is why it has to be typed;- nothing listens until the program asks. You cannot open a port from the command line, and no port survives a Power Reset;
- if the port cannot be bound — already in use, or an address that is not
yours — the program is told
ERROR. jnext never quietly picks a different port or a wider address.
Up to four programs can be connected at once, numbered from 1. Number 0 stays reserved for the program's own outbound connection, so opening a server never costs it the ability to dial out. A program can also hang up on any one of them by number, so a client that stops answering without disconnecting does not hold its place for the rest of the session.
An idle client is hung up on after three minutes. Something that connects
and then sends nothing for 180 seconds is disconnected, and the program is told
so — the same thing a real ESP-01 does, and the usual reason a debug session
dies while you were reading something else. A program that wants a different
answer sets one: AT+CIPSTO=<seconds>, anywhere from 0 to 7200, where 0 means
never hang up. The setting is forgotten on a reset, exactly as it is on
hardware, so it has to be sent each time.
Send it after the server is up, not before. AT+CIPSTO=<seconds> is
refused with ERROR while no server is listening, which is what a real ESP-01
does — so it belongs after AT+CIPSERVER=1,<port>, never between that and
AT+CIPMUX=1. A program that sends it too early and does not check the answer
keeps the 180-second default without being told. The query form,
AT+CIPSTO?, answers at any time.
Changing the address the module claims¶
The emulated module tells the program its address is 192.168.1.50. A program
that recognises its own address — a debug stub checking that the address it
advertised is the one a debugger dialled — has to be run against the address it
expects, so that value can be replaced:
jnext --esp --esp-ip-address 10.0.0.42 stub.nex
That is what AT+CIFSR and AT+CIPSTA? then answer. It is only what they
answer: nothing listens on that address, no traffic goes near it, and the
emulated Next cannot be reached at it. The address something can actually
connect to is --esp-listen-address, which is a real socket and a real security
boundary; this one is a string in a reply.
A name is refused (the address a program is told it has should not depend on
DNS), and so is 0.0.0.0, because that is what the module reports when it has
no network at all — see the next section.
Testing what happens when the WiFi goes away¶
A program that has to cope with the Next dropping off the network — a debug stub that advertises an address, say — cannot be tested against a module that is never off it. Two options schedule an outage, counted in emulated frames, so an automated run can execute that code path:
jnext --headless --esp stub.nex \
--esp-delayed-disassociate-frames 300 \
--esp-delayed-associate-frames 900 \
--delayed-automatic-exit-frames 1200
At frame 300 the module loses its association and AT+CIFSR — the command a
program asks for its own address — starts answering +CIFSR:STAIP,"0.0.0.0"
instead of 192.168.1.50. At frame 900 it is back, with the same address it
had before, because a short outage does not normally move it. Both edges are
reported in the log, so a script can see them without asking the program.
The second option needs the first (the module starts on the network, so there is nothing to rejoin), and its frame number has to be the larger of the two.
The address can come back different. --esp-ip-address-after ADDR sets what
AT+CIFSR reports once the outage ends:
jnext --headless --esp stub.nex \
--esp-ip-address 192.168.1.50 --esp-ip-address-after 192.168.1.77 \
--esp-delayed-disassociate-frames 300 --esp-delayed-associate-frames 900 \
--delayed-automatic-exit-frames 1200
That is what makes a program's stale-address handling testable at all. Across an outage that returns the same address, a program that reads its address once at start-up and one that re-reads it afterwards behave identically — so a test cannot tell them apart. Give it a different address on the way back and only the program that re-reads gets it right. A short outage normally does return the same address, which is why that stays the default.
The address report is the only thing that changes. Connections already open keep running, new ones still open, and nothing unsolicited is sent to the program. That boundary is deliberate: what a real module does to traffic while its access point is down has not been measured, and inventing an answer would mean a test that only measures the invention. What has been measured is a program listening for connections on a real Next surviving a five-minute outage without noticing.
Setting up the Wi-Fi by hand¶
The ZX Spectrum Next's own WiFi documentation walks you through bringing the module online from a terminal, and that whole session works:
| Command | What jnext answers |
|---|---|
AT+CWMODE? |
the current mode — 1, station, at power-on |
AT+CWMODE=1 |
OK. Mode 3 (station + access point) also works |
AT+CWLAP |
one access point: jnext's own JNextWifiHost |
AT+CWJAP="yournetwork","yourpassword" |
WIFI CONNECTED, WIFI GOT IP, OK |
AT+CIFSR |
the station address, 192.168.1.50 by default |
AT+CWQAP |
WIFI DISCONNECT, and the address goes away |
Any network name is accepted. jnext's Wi-Fi is synthetic — nothing routes through it, and your real connection is the host machine's — so there is no password to get wrong and no network that is out of range. Type whatever your program or your notes expect.
AT+CWMODE=2 is access-point-only, which on real hardware means the module
stops being a station. jnext models that: after it, AT+CIFSR reports
0.0.0.0 and a connection attempt fails, until you set mode 1 or 3 again.
AT+CIPSTATUS lists the connections that are open right now, including any a
program is serving, which is the companion to closing one by number with
AT+CIPCLOSE=<id>.
AT+CIPDOMAIN="somewhere.example" looks a name up without connecting to it
and answers +CIPDOMAIN:<address>. It answers only for a host a connection
would have been allowed to reach, and it needs a station for the same reason a
connection does — after AT+CWMODE=2 or AT+CWQAP there is no access point to
ask, so it answers ERROR.
If you restricted the guest with --esp-allow, or if the name points at an
address jnext blocks anyway (your own machine, a cloud metadata service), the
lookup answers DNS Fail and ERROR — the same reply it gives a name that does
not exist, so a program cannot use it to map what is being blocked. The run's
own log says which of the two it was.
AT+PING="somewhere.example" sends a real ICMP echo and answers with the
round-trip time in milliseconds. It needs no special privileges and no ping
program — jnext opens the same kind of socket the system's own ping uses — so
it works in the Flatpak build as well, where there is no ping to run.
It follows the same rules as everything else: the host must be one
--esp-allow permits, it must not be an address jnext blocks anyway (your own
machine, a cloud metadata service), and the module needs a station, so it
refuses after AT+CWMODE=2 or AT+CWQAP. A blocked host and a host that
simply did not answer both reply +timeout and ERROR, so a program cannot
use the difference to work out what is being blocked — and on a machine whose
configuration forbids the socket outright, that is the same answer you get.
AT+CIPSNTPCFG=1,<timezone>[,"server"] and AT+CIPSNTPTIME? let a program set
the clock from the internet. jnext queries a real NTP server, so the answer
is the true time.
That deliberately ignores --rtc. If you pinned the emulated clock with
--rtc so screenshots come out the same every run, a program that asks SNTP
still gets wall-clock, because that is what a time server returns. The two
clocks disagree on purpose — if you need a reproducible date, read the
machine's own clock rather than SNTP.
The timezone is in hours, from -11 to +13, and is required when you enable
SNTP. The server must be one --esp-allow permits, like any other host. Until
a query succeeds — and if one fails, or the server is blocked — the module
reports Thu Jan 01 00:00:00 1970, which is what a real module that has never
synchronised reports, so a program that polls until the year looks sensible
keeps working.
What is not emulated yet¶
The command set covers the Next's own documented setup session and everything evidenced in software that actually runs on a Next. Beyond that:
- transparent mode (
AT+CIPMODE=1), where the module stops interpreting AT commands and every byte becomes data.AT+CIPMODE=0— asking to stay in the normal mode jnext is always in — does work; - TLS, so a program cannot make an
httpsrequest; - being an access point —
AT+CWMODE=2records that the station is gone, but nothing can connect to jnext over Wi-Fi. A program that wants to be connected to should listen withAT+CIPSERVERinstead; - saving settings —
AT+UART_DEFbehaves likeAT+UART_CUR, and nothing a program configures survives anAT+RST.
UDP is emulated, including AT+CIPSTART="UDP",… with its optional local
port. That is what newt needs to set the clock from an internet time server,
and .newt sntp 0 pool.ntp.org works. What is not emulated there is UDP
"modes" 1 and 2, which let the far end of the link change to whoever last sent
a packet; the emulated module answers ERROR rather than pretending.
None of the rest has a consumer in current Next software, so the rest of the ESP-01 firmware specification is not emulated.