JNEXT
Skip to content

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

NXtel connected to the live BBS through the emulated ESP-01

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.cfg file 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/8 and ::1: daemons on your own machine;
  • link-local — 169.254.0.0/16 and fe80::/10;
  • cloud metadata endpoints — 169.254.169.254, 100.100.100.200 and fd00:ec2::254;
  • the unspecified addresses 0.0.0.0/8 and ::;
  • 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 is not in the allowlist, check your --esp-allow spelling against the name the log shows the program asking for, or drop the restriction to confirm that is what it is. If it is address refused by policy, the program asked for something on the always-refused list and no --esp-allow will 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 enabled line. If it is absent the ESP is off — --no-esp beats 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 ADDR widens it. --esp-listen-address 0.0.0.0 means 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 https request;
  • being an access point — AT+CWMODE=2 records that the station is gone, but nothing can connect to jnext over Wi-Fi. A program that wants to be connected to should listen with AT+CIPSERVER instead;
  • saving settings — AT+UART_DEF behaves like AT+UART_CUR, and nothing a program configures survives an AT+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.