Accessing the Internet with an Apple //c

Apparently powering on the Apple //c in December re-ignited a spark of « let’s do cool things on a forty-years old piece of hardware », and I set out to a new goal: be able to access (parts of) the Internet on that 1MHz, 9″ green-phosphor display thing.

After investigating what existed, I found out I was not the only one with such dreams, and the Apple 2 has been online since a while. For ][e and IIgs, there exist dedicated hardware Ethernet cards, like the Uthernet cards. But that is not an option on the //c, which has no expansion port.

I found out that a bunch of geeks are working on Fujinet, a multi-purpose ESP32-based device that plugs into… The SmartPort external drive port of (almost) any Apple 2, but

a) not mine, a ROM 255 model. I could easily upgrade the RAM though, but

b) it’s kind of a prototype right now and the hardware is hard to buy, and I’m not a hardware guy.

Also, I was kind of sold on the idea of not needing exotic hardware. So I decided to use the serial port. I already had success communicating over serial with my Apple //c using cc65’s serial driver, a bit of a serial lib, and tools I wrote to use them.

I already have experience with libCURL from the days when I participated a lot to Claws-Mail, and CURL is the de-facto free software http – and even network – library, so I started a little « SURL » thing, the S standing for Serial.

SURL has two parts, a process that runs on a modern PC (or Rasberry) running Linux, listens on the serial port, and takes orders and sends replies to the Apple //c. On the Apple //c is a little library, or rather a set of functions, to send those orders and receive those replies.

Here is basically how it works:

The surl_server is built and installed using:

$ git clone
$ cd a2tools/src/surl-server
$ sudo apt install libjq-dev libgumbo-dev \
  libjpeg-dev libpng-dev libcurl4-gnutls-dev
$ make && sudo make install
$ sudo surl-server
... set the parameters in /etc/surl-server/tty.conf
$ sudo systemctl start surl-server

And in the client Apple 2 program :

1) surl_start_request(char method, char *url, char **headers, int n_headers). It takes a method and URL, and optionnally headers. Currently implemented methods are GET, POST, PUT, DELETE (which are used for FTP and HTTP urls) and RAW (which is used to connect to an arbitrary host:port and just pass bytes back and forth).

2a) For POST and PUT requests, one then sends the request data:

We are soon going to surl_send_data() a Mastodon post

surl_send_data_params(size_t total, int raw)

surl_send_data(buffer, len)

the raw parameter is for HTTP POST requests only and informs surl-server of whether we’re going to send data to pass as-is, like client_id=abc&client_secret=xyz&field_name=this%20is%a%20test ; or if we’re going to pass a buffer of non-encoded data that we want surl-server to encode for us, consisting of an alternance of parameter / value lines:

this is a test
this is a multi-line\r\nvalue

In that second case, only line breaks have to be encoded on the Apple //c as \r\n. It is also possible to instruct the surl-server to re-encode a value passed from one encoding to UTF-8. This is useful for us people with accentued characters. For example, the french character set used on the Apple 2 is named « ISO646-FR1 » and the string « J’apprécie les moments où on se repose à l’ombre » is encoded as « J’appr{cie les moments o| on se repose @ l’ombre » and this is how it will go out on the serial port. In some cases, you’ll want some parameters transcoded (like a text), but not others (like an email address). To activate transcoding on a field, append |TRANSLIT|<CHARSET> to the field name:

J'appr{cie les moments o| on se repose @ l'ombre

2b) for GET, DELETE or RAW request, there is no request body to upload.

3) the surl-server now performs the requests and sends the result as indicated by the remote server. You can read it using surl_read_response_header() which will set the relevant fields:

  unsigned int code;
  size_t size;
  size_t header_size;
  char *content_type;

A shortcut command to verify that the response is OK is surl_response_ok(), a simple wrapper that checks that (response != NULL && response->code >= 200 && response->code < 300).

We don’t have to check for codes 30x as the surl-server will follow redirections. We usually don’t have to bother with headers either as the surl-server will handle the potential Set-Cookie / Cookie headers.

Now we’re there, the surl-server is going to hold on to that response and wait for either commands or a new request.

The various commands available are:

surl_receive_data(surl_response *resp, char *buffer, size_t max_len) – will send back as much as max_len bytes of the response’s data, starting at 0 and then continuing with the next bytes when called multiple times.

After having surl_receive_data() on the home climate control system

surl_receive_headers(surl_response *resp, char *buffer, size_t max_len) – the same, only with headers data.

surl_receive_lines(surl_response *resp, char *buffer, size_t max_len) will do the same as surl_receive_data, but stop earlier than max_len to make sure you receive complete lines separated by \n and not half a line, apart if we reach a line longer than max_len, in which case it will be cut short.

Using surl_receive_lines() to display CSV data from the home energy monitoring system

surl_find_line(char *buffer, size_t max_len, char *search_str) – can be used to search for a pattern in the response’s data, and receive at most max_len bytes starting at the first match of this pattern. I use it for html-scraping. Note that multiple calls to surl_find_line() always returns the first match (as of now).

surl_find_line() proving useful to grep authenticity_tokens out of Mastodon login pages

surl_get_json(char *buffer, size_t max_len, char striphtml, char *translit, char *selector) – when the response’s Content-Type is application/json, returns the result of jq parsing that response using the selector passed as parameter. The striphtml parameter instructs the surl-server to replace HTML contents with plain-text, and the translit parameters instructs the surl-server to convert the charset from UTF-8 to the specified charset (like ISO646-FR1, you guessed it).

After quite a bit of surl_get_json() on Mastodon’s APIs

Differences with the jq command-line tool is that strings are not enclosed in quotes, and null fields are omitted. If your selector includes a field that may be null, put it last so you can check the numbers of lines received in response.

4) You may now surl_response_free() your response and do another request.

There is a special case for the « HGR » command, which has no surl_prefixed function to call. This command can be used on png or jpg responses to have the surl-server convert the image to the HGR format and send it back to the Apple //c. It has no surl_prefixed function to go with it because memory is really sparse in an Apple 2 program having 8kB of RAM reserved for the HGR page. It is usable in the following way:

int len;
char monochrome = 1;
if (simple_serial_getc() == SURL_ERROR_OK) {
  simple_serial_read((char *)&len, 2);
  len = ntohs(len);

  if (len == 8192) {
    char *hgr_page1 = (char *)0x2000;
    simple_serial_read(hgr_page1, len);
My Mastodon profile picture, as seen in 1980

Another special case for the SURL_METHOD_RAW request, taking « host:port » as an URL: once the response code has been checked to be 100 (Continue), the surl-server has connected to the remote host and you can now read and send bytes using the serial interface, be it cc65’s ser_get() and ser_put(), or simple_serial_* functions.

using SURL_METHOD_RAW to telnet to the serial proxy, ssh to my laptop and run an ansible-playbook

Building for the Apple //c: The surl library code is split to minimize code footprint and you can link only the files containing the functions you use. For example, a program only handling JSON and POST uploads would #include « surl.h » and compile with:

cl65 -t apple2enh myprogram.c \
 -o myprogram \
 ../lib/simple_serial.c ../lib/surl_core.c  \
 ../lib/surl_send.c ../lib/surl_get_json.c

I have uploaded a video with a demo and a bit more explanations if you’d like:

Conclusion: All of this was very fun to put together. Keeping the available memory to an acceptable level was also a challenge for my Mastodon application, requiring a lot of tricks I learnt along the way: Splitting the functionality across programs calling each other with parameters (at the expense of more disk usage due to the duplicated serial and surl code… But this still fits on a single 140kB floppy!) ; Using #pragma(code-name, « LOWCODE ») for the image viewer to fit as much possible code under the HGR page ; #pragma(code-name, « LC ») for most of the serial and surl code so that this code doesn’t live in RW memory but rather in the 3072 bytes available on the Language Card. Factorizing code, and not inlining functions, at the expense of speed. Reducing the stack size to 1kB instead of the default 2kB. And quite a load of low-level, few-bytes-at-a-time gains that, added together, proved useful.

I am quite sure this code could be ported to work with the Fujinet API and benefit from the much-faster speed of the SmartPort, even if as of now, I think, the Fujinet does not implement surl_find_line(), HGR, or charset conversion. If I get one of those devices someday, I may give it a try!