Bulletin Board Systems (BBS) seem to be making a comeback recently. Previously, we looked at how to get your Vice C64 Emulator dialling BBS and how it works on the new C64U. Now, let’s look at how we can create our own modern BBS from scratch!
Modern BBS, What’s That?
Back in the day, a BBS would be attached to a telephone line and users would connect via a physical modem. We won’t be doing that, not least because I don’t even have a landline any longer.
Instead, we’ll take the modern approach and have our BBS operate over the Internet. This allows any machine with the appropriate client software and an Internet connection to connect to the service, rather than dialling in over a phone line.
Under the hood, we swap the modem and serial connection for a TCP-based network connection, with the BBS running on an always-on server.
Why Code BBS? Why Now?

My guess is this is both a result of more and more internet-connected retro options appearing, plus the pushback against the billionaire controlled algorithm dictated social media tools we are increasingly getting tired of.
While there are a lot of tools already out there that allow you to set up a BBS of your own, here we focus on coding so of course I wanted to see how difficult it would be using modern programming tools.
Even if you don’t actually want to build a community or information service, the ideas and techniques involved are still valuable because the underlying concepts are equally valuable for other kinds of connected applications, including online multiplayer games.
How Bulletin Board Systems Work

Bulletin Board Systems, no matter which language they are programmed in, have two main aspects that need to be considered:
- The service itself – This is what will BBS users see, do, read, download, and so on. Much of the work of being a system operator (Sysop) is here, keeping the service up to date and managing user activity (Eg. banning trolls).
- Communications – This is the plumbing. Handling new connections and input, delivering the responses, closing connections cleanly.
There is, of course, a third component and that is the BBS user. The users will need compatible communications software and some way to make a connection to your BBS.
Most communications software is fairly standardised, for example ANSI escape codes for screen control and colours. Some BBSs, though, are built for specific machines, such as the Commodore 64 using PETSCII, so users got a more tailored experience on their hardware.
In this project, we’ll be running a Python program over TCP/IP and listening for connections on a chosen port. Messages will be sent back and forth between the server and the client as plain text, with larger blocks of pre-built content, such as welcome and menu screens, stored as files on disk.
TCP Services in Python
Helpfully, Python gives us everything we need to handle TCP connections via the built-in socket module.
For our BBS, each connected user will end up with their own socket. A socket is the “pipe” we use to receive keystrokes and send text back to their terminal.
There’s nothing to install, the required library ships with Python.
We can set up a basic TCP server that listens for incoming connections like this:
import socket
LISTEN_HOST = "" # empty string means all network interfaces
LISTEN_PORT = 6464
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((LISTEN_HOST, LISTEN_PORT))
server_socket.listen(256)
server_socket.setblocking(True)
# handle a single message, echo it back, then close the connection
while True:
connection, address = server_socket.accept()
input_buffer = connection.recv(1024)
print(input_buffer.decode("utf-8", errors="replace"))
connection.send(input_buffer)
connection.close()
This tells the operating system that our program should listen for incoming connections on a specific port number and echo back anything sent over that connection. In more technical terms, this is called binding the socket to a port.
To test our code we can use Telnet which is available for Windows, Mac and Linux. Unfortunately modern systems neglect to include this tool by default so you will probably need to download and install it.
Mac:
brew install telnet
Linux:
apt-get install telnet
Windows:
pkgmgr /iu:"TelnetClient"
After installing telnet, save this simple code as echo.py and run it with, for example, python3 echo.py. It will run and wait for your connection.
We can then test using telnet localhost 6464

At this point, if you already have your retro computer or emulator set up to dial a BBS (for example with the ZX Spectrum Next, Commodore 64 Ultimate or Vice emulator), you can also test it that way too!
Experimenting with ANSI Art
As I mentioned earlier, the lingo of bulletin boards is ANSI. Before BBS there were already terminals in business and education, connecting to mini and mainframe servers, that needed to control screen layouts and formatting.
You might recall we have encountered ANSI escape sequences before when coding for command line terminals.
These codes can be combined to create colourful screens and became a popular art form. If you want to create your own ANSI art there is a great (free) tool for unix-like systems called Durdraw and there is Moebius for Windows, Mac, and Linux.

Once you have a draft of your art created and saved as a file (select ANSI and UTF-8 then we can deliver that as a payload to our BBS users:

How? It’s just a text file, albeit one with some special codes embedded in it that are interpreted by the client software!
def send_ansi_file(connection, filename):
with open(filename, "rb") as f:
while True:
data = f.read(1024)
if not data:
break
connection.send(data)
time.sleep(0.01)
What if your computer or communications software can’t interpret the full ANSI code and character set and you get a screen full of garbage back? In that case you will want to provide a tailored experience …
Serving PETSCII from Your Python BBS
I was going to code up helper functions for serving PETSCII, but then I found someone had done the hard work for us a few years ago and shared on Github.
We will not use the full project here because it relies on setting up a database and I am not sure it works ‘out of the box’ as presented, but feel free to check their code out.
You can grab the entire project or just the funct.py file. If you do download the whole thing you get the benefit of a couple of example .seq PETSCII art files.
Just as in our ANSI example above, the PETSCII art is delivered via static files, a byte at a time:
def send_seq(connection, filename):
print("sending seq file", filename)
with open(filename, "rb") as f:
nb=b''
byte = f.read(1)
while byte:
nb = byte
connection.send(nb)
byte = f.read(1)
What is really helpful in the library though is a bunch of helper functions, such as PETSCII encoding, and the cursor function that allows you to set colours and cursor positions.

For example, we can clear the user’s screen using connection.send(cbmcursor("clear"))
To create PETSCII art we can use our old friend petscii.krissz.hu and save our creations as .seq files.

Once we have the file saved in our project directory, we can serve the PETSCII art instead of the ANSII screen from earlier. In a real BBS you might give users the option, but for simplicity we can just switch out the welcome routines:

C64/Commodore 64 Ultimate Swiftlink Modem Client

Commodore BASIC, being quite slow to execute, is not the best choice for timing-dependent communications code, but with Swiftlink emulation (and especially the C64U) it is still possible to create useful connected programs.
While using Vice you will need to keep the communication speed low (line 170), just as with a real C64, but you do have the advantage of speeding up the emulation so you are not sat waiting quite as long.
10 rem ------------------------------------------------------------
20 rem SWIFTLINK TCP TERMINAL (C64 BASIC V2)
30 rem Uses direct PEEK/POKE to the SwiftLink 6551 ACIA
40 rem ------------------------------------------------------------
50 rem ACIA REGISTER ADDRESSES (SWIFTLINK AT $DE00)
60 dr=56832 : rem data register (read = receive, write = transmit)
70 sr=56833 : rem status register (flags: rx ready, tx ready, errors)
80 cm=56834 : rem command register (parity, echo, DTR, interrupts)
90 ct=56835 : rem control register (baud rate, word size, stop bits)
100 rem CLEAR SCREEN AND SET TEXT COLOUR
110 print chr$(147);chr$(5);"swiftlink term"
120 rem RESET ACIA BY WRITING TO STATUS REGISTER
130 poke sr,0
140 rem CONTROL REGISTER
150 rem 31 = 8 data bits, 1 stop bit, internal clock,
160 rem 19,200 baud setting doubled by SwiftLink crystal = 38,400
170 poke ct,16:rem poke ct,31 for full speed on c64u
180 rem COMMAND REGISTER
190 rem no parity, no echo, DTR on, receive enabled
200 poke cm,9
210 rem SHORT DELAY TO LET HARDWARE SETTLE
220 for i=1 to 500:next
230 rem SEND "AT" TO WAKE UP TCP MODEM EMULATION
240 rem leading CR ensures clean command state
250 ts$=chr$(13)+"at"+chr$(13)
260 gosub 700
270 rem DIAL TCP SERVER (IP:PORT)
280 ts$="atdt localhost:6464"+chr$(13)+chr$(13)+chr$(13)
290 gosub 700
300 t$=""
310 print "connected"
320 rem ------------------------------------------------------------
330 rem MAIN TERMINAL LOOP
340 rem - read keyboard and send characters
350 rem - poll ACIA for received data and print it
360 rem ------------------------------------------------------------
370 rem READ ONE KEY (NON-BLOCKING)
380 get a$
390 rem IF A KEY WAS PRESSED, SEND IT
400 rem ASC() CONVERTS CHARACTER TO BYTE VALUE
410 if a$<>"" then b=asc(a$):gosub 800
420 rem CHECK STATUS REGISTER
430 rem BIT 3 (VALUE 8) = RECEIVE DATA AVAILABLE
440 s=peek(sr)
450 if (s and 8)=0 then 370
460 rem READ RECEIVED BYTE
470 c=peek(dr)
480 rem NORMALISE LINE ENDINGS
490 rem MAP LF (10) TO CR (13) FOR C64 DISPLAY
500 if c=10 then c=13
510 rem PRINT RECEIVED CHARACTER
520 print chr$(c);
530 goto 370
700 rem ------------------------------------------------------------
710 rem SEND STRING TS$ CHARACTER BY CHARACTER
720 rem ------------------------------------------------------------
730 for i=1 to len(ts$)
740 b=asc(mid$(ts$,i,1))
750 gosub 800
760 next
770 return
800 rem ------------------------------------------------------------
810 rem SEND ONE BYTE IN B
820 rem WAIT UNTIL TRANSMIT REGISTER IS EMPTY
830 rem BIT 4 (VALUE 16) = TRANSMIT READY
840 rem ------------------------------------------------------------
850 s=peek(sr)
860 if (s and 16)=0 then 850
870 poke dr,b
880 return
Full Python BBS Code (so far!)
Here is the Python BBS server code so far. While it is not complete, it is already pretty useful. If you have been following my tutorials in the reborn Compute Gazette you will see a tutorial where I use code like this as the basis of a game that gets fresh data over the internet …
import socket
import time
from funct import *
LISTEN_HOST = "" # empty string means all network interfaces
LISTEN_PORT = 6464
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((LISTEN_HOST, LISTEN_PORT))
server_socket.listen(256)
server_socket.setblocking(True)
def send_ansi_file(connection, filename):
with open(filename, "rb") as f:
while True:
data = f.read(1024)
if not data:
break
connection.send(data)
time.sleep(0.01)
# handle a single message, echo it back, then close the connection
while True:
connection, address = server_socket.accept()
# send the welcome screen file
# send_ansi_file(connection, "welcome.ans")
connection.send(cbmcursor("clear"))
send_seq(connection, "seq/welcome.seq") #sends a seq file to screen
cursorxy(connection,5,10)
connection.send(cbmcursor("white"))
connection.send(b"options:\n")
time.sleep(.5)
# wait for a message, echo it back, then close the connection
input_buffer = connection.recv(1024)
print(input_buffer.decode("utf-8", errors="replace"))
connection.send(input_buffer)
connection.close()


C64 Ultimate Review + What’s Next?