Using just about every technology since the telegraph to get a 1999 palmtop online at 35,000 ft:
EPOC browser → HTTP → TCP/IP → PPP → RS-232 at a blistering 115,200 baud → USB serial adapter → Android phone running slirp (a 1995 SLIP/PPP emulator) → Wi-Fi → the aircraft’s cabin server → a GPS-steered satellite antenna → a satellite → a ground station → the internet. And back. To load one text-only news page.
That was the short version I posted from seat 7A of KLM Airbus 321 bound HEL-AMS – attention-seeking social media post FTW! Here’s the long version: what’s actually going on in that chain, and how I ended up writing an Android app to make it work:


The stack, top to bottom
- HTTP: the EPOC R5 Web browser, plain HTTP 1.1 only, because it’s 1999 and TLS 1.3 hadn’t been invented…
- TCP, UDP and IPv4: EPOC’s own TCP/IP stack, UDP mostly for DNS.
- PPP: link and address negotiation (LCP and IPCP), with HDLC-style framing and byte stuffing.
- RS-232: 115,200 baud 🔥 with RTS/CTS flow control, over a null-modem cable.
- USB: a Prolific PL2303 serial adapter plugged into the phone’s USB-C port.
- PPP Link, my Android app. It runs slirp, a 1995 tool that turned dial-up shell accounts into SLIP/PPP connections. Slirp unpacks the palmtop’s TCP/IP and rebuilds every connection as an ordinary Android socket.
- Android and the Linux kernel’s TCP/IP stack, then 802.11 Wi-Fi 6 to the cabin access point.
- The aircraft’s satellite terminal, whose antenna stays locked on using the plane’s satellite-navigation and inertial data.
- Up to a satellite, down to a ground station, out to the internet, and all the way back again.
The palmtop in question is an Ericsson MC218, a rebadged Psion Series 5mx running EPOC Release 5. It has a 36 MHz ARM processor, a beautiful keyboard and a serial port. (It has RS232 serial or iRDA as its only comms links to the world… It does not have anything as modern as Wi-Fi/Bluetooth.) At the time of Psion’s collapse they were working on a variant of EPOC that supported Bluetooth, but that’s another story…
How it started
It began with PsiTerm, Dan Edge’s SSH client and mail program for the Series 5mx. It does modern crypto (curve25519, ChaCha20, TLS 1.3) on a 36 MHz ARM710, which is a ridiculous and wonderful thing in itself. PsiTerm can connect two ways: through a Wi-Fi modem on the serial port that understands ATDT host:port, or through the Psion’s own dial-up PPP stack.
I had a NodeMCU in a box somewhere that could become a Wi-Fi modem. But it needs RS-232 level shifting and its own power, and I wanted something simpler. My phone already has an internet connection and a USB port. Could the phone itself be the Psion’s ISP?
It turns out someone had already done most of the hard part. Mark Williams’ blog describes getting retro machines online with nothing but a £12 USB serial cable and an Android phone:
- TCPUART, an Android app, bridges the USB serial adapter to a local TCP port.
- Termux runs slirp, the 1990s userspace PPP server, which needs no root because it makes ordinary socket connections on the phone’s behalf.
- socat joins the two together.
I wrapped that recipe in a setup script and added a small fake Hayes modem front end, so the MC218’s normal dial-up connection would just work. Then I switched to the MC218’s “Direct Cable Connection”, which skips dialling and starts PPP straight away. Within an evening the MC218 was loading web pages. PsiTerm briefly insisted on connecting to 0.0.0.22, which turned out to be me putting only the port number in the hostname field, and the host name in the connection’s “name” field. D’oh!
It worked, but it was clunky: two apps to start in the right order, a port that occasionally clashed with something else, and Android’s battery management hungry to kill either of them.
One app instead of two
The obvious next step was a single Android app that does both jobs: talk to the USB adapter and run slirp. Google had been spamming me with ads about Gemini’s ability to “vibe code” Android apps, so I tried it, starting from a detailed specification written with Claude. The spec deliberately kept the proven core: the same slirp, the same arrangement as the Termux setup, just embedded. It used:
- usb-serial-for-android for the adapter. Its PL2303 driver supports RTS/CTS hardware flow control, which TCPUART couldn’t do and which matters at 115,200 baud.
- slirp compiled with the Android NDK, packaged as
libslirp.so. Android won’t execute files an app writes to its own storage, but it will execute native libraries shipped inside the APK. - A small JNI launcher that starts slirp on a pseudo-terminal, because slirp expects to talk to a real terminal.
- A foreground service so Android doesn’t kill the link when the screen turns off.
Gemini got there, eventually. Gemini’s “vibe coding an Android app” seemed pretty dumb to me: the road included all sorts of basic things – Java runtime mismatches, JDK version fights and Android Studio refusing to sync. There was also some genuinely poor code: it would “Start Link” without checking an adapter was plugged in, and the button kept saying STOP LINK even after the link had stopped. But it basically worked, which is a decent result for a first build.
I then had Claude review the code against the original spec, listing problems before changing anything. That review found fifteen issues. The worst were:
- Replugging the adapter never resumed the link. The USB events were handled by the screen, not the service, so they went nowhere once the screen was gone.
- Two USB readers at once. When the MC218 hung up and slirp restarted, the old read loop kept running alongside the new one and could swallow the start of the next connection.
- Stale processes. A quick stop and start could leave a ghost slirp, or have an old watcher kill the new session.
- USB permission was never actually requested, so starting from the app icon got stuck.
- The status card was placeholders. The adapter name, DNS server and byte counters were hard-coded strings.
The fix was a rewrite of the link engine. All session state now changes on a single thread, so start, stop and USB events can’t interleave. One reader owns the USB port for the whole session and survives slirp restarts. Every loop checks it still belongs to the current session before acting.
Bugs only real hardware finds
The re-written app built and ran (hurrah!). Then the real EPOC32 machine found four more problems, each one a nice little puzzle.
“Error: Bad IP”. The MC218 connected but couldn’t resolve names. Slirp’s log said Error: Bad IP at exactly the point it read its DNS setting. Slirp reads config lines with their trailing newline still attached and passes the address to inet_aton(). On Linux, glibc’s inet_aton() shrugs off the newline. Android’s Bionic C library rejects it. So slirp had no DNS server at all, and lookups only worked because thesion fell back to its secondary DNS. A three-line patch strips the newline. I proved it by building slirp on Linux against a deliberately strict inet_aton(): unpatched, it reproduced the exact error; patched, it set the DNS.
The link that wouldn’t die. Pull the MC218’s cable and the app kept proudly showing “Connected”. Nothing could notice, because the USB adapter was still plugged in. The fix was to turn on PPP’s own keepalives (LCP echo requests every 10 seconds). Now if the MC218 stops answering, slirp drops the link and restarts, ready for the next connection.
The sleeping MC218. Mid-session, with nothing unplugged, the link dropped. The log showed the app’s write-stall timer firing almost exactly five minutes after PPP came up. That’s the MC218’s auto switch-off. When it went to sleep, its serial port held the line low, and after ten seconds of no progress the app assumed the link was dead and restarted it. A sleeping MC218 isn’t a dead link. Now the app shows “not ready”, discards what it can’t send (PPP and TCP resend anything important) and carries on when the MC218 wakes. The keepalives still end things if it never comes back.
DNS that didn’t follow the network. Switch the phone from mobile data to Wi-Fi mid-session and lookups stalled. Slirp reads its DNS server once, at startup, and mobile operators’ DNS servers usually only answer from their own network. The fix was a small DNS relay inside the app. Slirp sends the MC218’s queries to the relay, and the relay answers each one through Android’s own resolver on whatever network the phone is using at that moment. That needed one more slirp patch, because Android apps can’t listen on the standard DNS port 53. A bonus: the MC218’s lookups now go through the phone’s Private DNS setting too.
From PsiLink to PPP Link
The app started life as PsiLink, became EPOC Link to cover Ericsson and other EPOC32 machines, and finally became PPP Link. Nothing in it is actually specific to EPOC32 devices. Any retro computer with a serial port and a PPP stack can use it: an old Mac, an Amiga, a Windows 95 laptop, a Palm. The EPOC setup guide became generic client settings:
| Setting | Value |
|---|---|
| Serial | Same speed as the app, 8N1, null-modem cable |
| Connection | PPP, either direct, or “dialling” a fake modem that answers any AT command |
| IP address | 10.0.2.15, or obtain automatically |
| DNS | 10.0.2.3, with 1.1.1.1 as backup |
| The phone itself | 10.0.2.2 |
The icon is a little clamshell palmtop on a charcoal background, with a serial square wave on its screen.
Before publishing anything I checked every line of borrowed code. It’s all permissively licensed:
- slirp is BSD-style, with the old “advertising clause”.
- Its TCP/IP code comes from 4.4BSD (UC Berkeley).
- The PPP code comes from Carnegie Mellon, the Australian National University, Gregory Christy and Juha Pirkola.
- MD5 is RSA Data Security’s reference implementation.
- The USB library is MIT, and the Android libraries are Apache 2.0.
The Gemini build had dropped slirp’s licence file, so that went back in, along with a full credits list and an open-source licences screen in the app. PPP Link itself is Apache 2.0. One fun constraint: those BSD advertising clauses are incompatible with the GPL, so the project can never be relicensed under it.
The plan is to publish on F-Droid, the open-source Android app store. F-Droid builds every app from source, so the remaining work is a reproducible build, store metadata and a build recipe. Google Play is possible too, though it now asks new developers for a two-week closed test with twelve testers.
What I learned
- Old code ages well. Slirp is thirty years old and still does its job. It needed one newline fix and a DNS tweak, not a rewrite.
- Start from what works. The Termux proof of concept was clunky but it worked, and it became the reference for everything after. When the app misbehaved, “does the Termux version do the same?” was always the first question.
- AI coding tools are good at volume, less good at concurrency. Gemini produced a working app with a lot of help. A separate review pass caught the threading, lifecycle and race-condition bugs that only show up when a cable gets pulled at the wrong moment.
- The real hardware is the test suite. Every one of the last four bugs came from plugging in the MC218 and doing something normal: unplugging it, letting it fall asleep, getting on a plane.
What’s next
- Publishing on F-Droid.
- Testing with other retro machines. If you have a vintage computer with a serial port and a PPP stack, I’d love to hear whether it works.
- PPP Link’s source and releases will be on my GitHub as zedstarr.
And I’ll keep using it at 35,000 ft, loading one lovingly text-only page at a time.

