Once I had a reasonable development environment, I could get my hands dirty and start coding. I decided to re-use a server from one of my other projects, so I only needed to take care of the client-side implementation. It consisted of two main parts:
- a library running inside the C64 WiFi Modem responsible for networking
- a chat app running on Commodore 64
Let’s take a closer look at how they were implemented
Networking with the C64WiFi Modem
The modem is responsible for handling network related functionality. It needs to be able to connect to the WiFi network and – once connected – allow to establish a connection with a web server. This functionality is provided by the ESP8266 chip that powers the C64 WiFi modem. The WiFi connection can be established using the
ESP8266WiFi library. HTTP requests can be sent to a web server using the
ESP8266HTTPClient and the
WebSocketClient can be used to communicate with the web server over a webSocket. To expose this functionality to external devices (clients) I created a library running on the C64 WiFi Modem (or, to be more accurate, on the ESP8266 chip) that implements a simple protocol that allows to start or stop WiFi, open a webSocket and send and receive messages over the webSocket. The client would send commands using the serial device and would receive a response containing the result of the operation. Once the webSocket is opened successfully, the client can also instruct the modem to send data to web server or receive data from a web server. The functionality offered by the library running on the modem is not geared towards any specific use case. In fact, testing it from a C-64 would be quite daunting and it was much easier and faster to create a Python client that used the
pyserial package to communicate with the modem.
Commodore 64 chat app
The chat app for the C-64 is much more complicated than the library for the C64 WiFi modem. It not only needs to provide the interface for the user but also takes care of all the communication. As I set out to start working on the implementation, I was worried that coding all of this up in assembly will end up in a horrible spaghetti code that no one – including myself – will be able to understand. To prevent that, I decided to go with a layered design where each layer is responsible to handle a single concern and can only communicate with the layer directly above or below. I identified the following layers:
+----------------------+ | application | +----------------------+ | SignalR | +----------------------+ | ESP | +----------------------+ | serial communication | +----------------------+ ... +----------------------+ | C64 WiFi Modem | +----------------------+
The serial communication layer is responsible for communication with the serial device – it knows how to open the device and allows reading incoming data. The ESP layer understands the protocol implemented in the library running on the C64WiFi modem. It knows how to create and send commands and interpret results. The SignalR layer uses the API exposed by the ESP layer to connect to WiFi, start a webSocket connection to a web server and communicate over this webSocket. It also has some understanding of the SignalR protocol – it knows how to initiate the SignalR connection, handle handshake or ignore “uninteresting” (or unsupported) messages from the server (e.g. pings). Finally, the application layer drives the execution of the entire application. It does that by setting up a raster interrupt which ensures that the application logic will be called repeatedly (60 times per second on NTSC systems, 50 times per second on PAL systems). Each time the interrupt handler is invoked it checks if there are any incoming messages and, if so, shows them on the screen. The interrupt handler also scans the keyboard and will take care of sending a message if the user pressed
<RETURN>. All the code responsible for UI is encapsulated in a dedicated UI module. Sending and receiving messages directly in the application layer is a bit messy because it also includes logic that encodes and decodes messages according to the SignalR protocol and the MessagePack format. As per the layering above this should ideally be part of the SignalR layer but doing this in the application layer proved to be easier to implement and faster to run (important due to the timing constraints related to running inside the raster interrupt handler which needs to finish within at most 16 ms). Given this was just a hobby project, I decided that this trade off was acceptable.
In addition to the layered design, I also settled on using several patterns that helped me avoid mistakes and a lot of debugging:
- arguments are passed to subroutines in registers, if possible
- subroutine results are passed in registers, if possible
- subroutines are not expected to preserve registers – the caller is responsible to preserve registers if needed
- Some zero-page locations have a specific purpose (e.g.
$fb/$fcvector always points to the send buffer) while other (e.g.
$fd/$fe) can be used for any purpose and by any subroutines so no code should use them without proper initialization
During the implementation I found that the project would get significantly easier if I changed some of the assumptions I originally made. Initially, I planned to communicate with the server using the long polling. Long polling (a.k.a. Comet) is a technique where the client sends an HTTP request and the server keeps it open until it has anything to write, or timeout occurs. Once the HTTP request is closed the client immediately sends a new HTTP request. This pattern is repeated until the logical connection is closed. Sending data to the server requires sending a separate HTTP request. When researching this option, I found that the
ESP8266HTTPClient is blocking and does not allow working with more than one connection at the same time. I needed something better and I found the
WebSocketClient library. Using webSockets was a much better option and saved a ton of work. I no longer had to deal with coordinating and restarting Http requests (which can get tricky) and establishing the connection to the server was greatly simplified as SignalR requires an additional
negotiate request for the Long Polling transport but not for the webSocket transport.
There was one more place where I switched from text to binary. The protocol I created to talk to the C64 WiFi modem was initially text based. The biggest advantage was that I could test it without using any specific tools – I would just start
screen or PuTTY and could type commands directly from the keyboard to see if things work. Interpreting data received from the modem in assembly turned out cumbersome (but worked!) but after I decided to move from JSON to MessagePack using a text based protocol for the communication with the modem was no longer an option. I had to create a couple of tools in Python to make testing easier but the simplification it yielded in the chat app was totally worth it.
The implementation for the Cloud Enabled Commodore 64 required aligning many stars. Keeping clear boundaries, using the right tools and a bit of luck was essential to complete the project successfully. Revisiting assumptions, adjusting the direction and finding simpler solutions cut a lot of time and effort. Next time we will take a look at more ideas I had and how they could have shaped the project if I decided to use them.