Willie’s BBS is a bulletin board system that enables a home computer to act as a dial-up BBS, communicating with callers over a phone line using port 119 for modem control and port 115 for serial character output. The system supports up to 50 registered users with first/last name login, password protection, private messaging, a chat mode between the caller and the sysop, and a utilities menu for managing the user file. Machine code routines loaded from a separate cassette tape (“bbsmc”) handle low-level serial I/O, referenced via named labels such as output, input, and statchk. A software connect-time timer is implemented by reading the system’s three-byte frame counter at addresses 23672–23674 and converting it to hours and minutes. The program saves itself back to tape with SAVE "tsbbs" LINE 10 to allow the sysop to update the stored copy after making changes.
Program Analysis
Program Structure
The program is organized into clearly labeled functional blocks using REM comments. Execution flows through a series of subroutines and GO TO labels rather than a tight main loop. The primary sections are:
- Lines 10–50: Initialization — loads machine code tape, gets the date, waits for a keypress to go online.
- Lines 1000–1090: Phone answer loop — polls port 119 for an incoming ring, with an option to save the program back to tape.
- Lines 2000–2070: Warm start and main menu — sets online timer, calls logon/name subroutines, presents the main menu.
- Lines 3000–3110: Read messages — scans the message array for messages addressed to the current user.
- Lines 4000–4145: Leave message — finds a free slot, collects recipient and message text, stores it.
- Lines 5000–5130: Quit/hang up — sends goodbye text, resets modem port, returns to auto-answer loop.
- Lines 6000–6340: Logon bulletin and utilities sub-menu (change user file, list users).
- Lines 7000–7230: Chat mode — bidirectional character-at-a-time terminal chat between sysop and caller.
- Lines 7235–7265:
getkeyinput subroutine — collects a string from the caller via serial port. - Lines 7400:
clssubroutine — clears screen and sends control codes to remote terminal. - Lines 8000–8440: User identification — new user registration or existing user login with password.
- Lines 9990–9993: Timer subroutine — computes elapsed online time from system frame counters.
Machine Code Usage
The program loads a companion machine code file named bbsmc from cassette at line 30 using LOAD "bbsmc" CODE. Three entry points are referenced by label throughout the program:
output— called viaRANDOMIZE USR outputto prepare or clock out a character; typically followed immediately byOUT 115, CODE p$(d)to send the byte serially.input— called viaLET c=USR inputto receive a single byte from the remote terminal.statchk— called viaLET a=USR statchkin the chat loop (line 7100) to check serial status: the return value is interpreted as a bitmask where 1 = transmit ready, 2 = receive ready, 3 = both.
Port 119 is used for modem control (off-hook, answer, hangup commands), and port 115 is used for direct byte output to the serial line. These port assignments are consistent with an RS-232 or modem interface card for this platform.
Key BASIC Idioms
Several characteristic BASIC idioms appear throughout:
- Named line targets: Labels such as
inputcom,quit,read,leave,chat,autoans,logonbull,inname,cls,nl,sm,getkey, andtimerare used inGO TOandGO SUBstatements. These resolve to actual line numbers at runtime and serve as readable structured-programming anchors. - DATA/RESTORE for menus: Lines 2007–2020 and 6105–6115 use
DATAstatements to store menu text strings, thenRESTOREto the specific line andREADin a loop. This avoids long chains of individualPRINTstatements. - Boolean arithmetic: Line 9992 uses the expression
"0" AND hr<10to conditionally prepend a leading zero to hours and minutes — a standard Sinclair BASIC trick exploiting the fact that a true condition evaluates to 1 (selecting the string) and false to 0 (yielding an empty string). - ON ERR: Used at lines 2036, 5001, and 7001 to trap errors and redirect to
quitor to executeRESET, providing a safety net for unexpected runtime errors during a live call.
Data Structures
User and message data are maintained in string and numeric arrays (declared implicitly or by DIM calls assumed to be in the machine code bootstrap or an earlier initialization block not shown). The key structures are:
| Variable | Purpose |
|---|---|
f$(x) | First name of user x |
l$(x) | Last name of user x |
w$(x) | Password of user x |
z$(x) | Date of last call for user x |
t(x) | Call count for user x |
w(x) | Waiting message count for user x |
m$(u) | Message slot u — a fixed-width string: bytes 1–32 = To, 33–64 = From, 65+ = body |
m(u) | Flag: 1 = message in use, 0 = free |
un | Current user index |
tu | Total registered users |
u$ | Current user’s full name |
Messages are stored as fixed-width slices of a string array: m$(y, 1 TO 32) holds “To:”, m$(y, 33 TO 64) holds “From:”, and m$(y, 65 TO ) holds the body, terminated by CHR$ 3 (Ctrl-C / ETX).
Chat Mode Implementation
The chat mode at lines 7000–7163 implements a full-duplex character terminal. The status-check machine code routine is polled in a tight loop at line 7100, and receive-ready and transmit-ready flags are extracted from the return value using bitwise AND idioms. Received characters are printed locally and echoed; a received CHR$ 3 (Ctrl-C) causes an exit back to the main menu. On the transmit side, INKEY$ is polled and any local keypress is both printed and sent via OUT 115. The escape from chat for the sysop is detected at line 7161 by checking for CODE i$ = 195, which corresponds to the “NOT” or Symbol Shift key combination.
Connect-Time Timer
Lines 2003 and 9990–9993 implement a software timer. At logon, the three frame-counter bytes at addresses 23672, 23673, and 23674 are zeroed via POKE. The timer subroutine reads them back, reconstructs a 24-bit value, divides by 60 (frames per second) to get seconds, then computes hours and minutes. The result is read back from the screen character positions using SCREEN$ into the string array x$ so it can be transmitted to the caller as the connect-time string x$.
Notable Techniques
- The sysop-page sequence at line 7006 uses a counted loop of 128 iterations, printing a dot and beeping each time the caller’s key is not pressed, providing a visible “paging” progress indication while audibly alerting the sysop.
- Line 1025 allows the sysop to press “S” while scanning to immediately re-save the program to tape as
tsbbs, providing a convenient in-situ backup mechanism. - The
getkeysubroutine (lines 7237–7265) handles backspace (CHR$ 8) by shorteningr$using the idiomr$( TO (LEN r$-(LEN r$>0))), which safely prevents underflow to a negative length. - The new-user cap check at line 8127–8128 has a logic issue: when
tuexceeds 50, the code decrements it but then falls through toGO SUB smandGO TO quiton the same logical block, which works correctly only if those statements are on the same line as the decrement — as written, line 8128 unconditionally callsGO SUB smandPAUSE 300: GO TO quitregardless of whether the user limit was hit, meaning every new user registration triggers those lines. The conditional logic is incomplete because theIFon line 8127 does not extend to line 8128. - Password comparison at line 8410 uses
w$(un, 1 TO LEN r$)rather than a full equality check, meaning a caller who enters only the first few characters of their password will be authenticated — a security weakness inherent in the substring comparison approach.
Content
Image Gallery
Source Code
10 CLS
20 PRINT AT 5,0;"Load BBSMC tape and press ""PLAY"""
30 LOAD "bbsmc"CODE
35 PRINT "Loaded...Enter Date"
37 INPUT "DATE: DD/MM ";d$
40 PRINT ,,"PRESS ANY KEY TO GO ONLINE..."
50 PAUSE 0
1000 REM ANSWER PHONE
1002 OUT 119,34
1004 OUT 119,0
1006 PRINT AT 5,4;"SCANNING PHONE LINE..."
1010 PRINT AT 10,6;"PRESS ""S"" to update cassette"
1012 PRINT TAB 6;"PRESS ""C"" ANYTIME TO FORCE CALLER INTO CHAT MODE"
1020 LET a=IN 119
1025 IF INKEY$="S" OR INKEY$="s" THEN SAVE "tsbbs" LINE 10
1030 IF a=5 THEN GO TO 1020
1080 REM RING IN
1090 OUT 119,2: OUT 119,34
2000 REM WARM START
2001 OUT 119,64: OUT 119,123: OUT 119,55
2002 REM SET ONLINE TIMER
2003 POKE 23672,0: POKE 23673,0: POKE 23674,0
2004 GO SUB logonbull: GO SUB inname
2005 REM MAIN MENU
2006 GO SUB cls
2007 DATA " Willie's TS-BBS"," ","<R>ead your messages","<L>eave a message","<C>hat with SYSOP","<U>tilities Menu","<T>ime on since logon","<H>ang Up"," ","Choice?:"
2010 RESTORE 2007
2020 FOR p=1 TO 10: READ p$: GO SUB nl: GO SUB sm: NEXT p
2036 ON ERR GO TO quit: GO SUB getkey: LET c$=r$
2037 IF c$="U" OR c$="u" THEN GO TO 6100
2040 IF c$="r" OR c$="R" THEN FOR x=1 TO 2: GO SUB nl: NEXT x: GO TO read
2041 IF c$="t" OR c$="T" THEN GO SUB timer: LET p$="Connect time HH/MM: ": LET p$=p$+x$: GO SUB nl: GO SUB sm: PAUSE 100: GO TO inputcom
2045 IF c$="c" OR c$="C" THEN GO TO chat
2047 IF c$="H" OR c$="h" THEN GO TO quit
2050 IF c$="l" OR c$="L" THEN GO TO leave
2070 GO TO inputcom
3000 REM read messages
3007 LET w(un)=0
3010 LET p$="Scanning message base.....": GO SUB sm
3020 FOR u=1 TO 50
3025 LET r$=m$(u,5 TO (LEN u$+4)): IF r$=u$ THEN GO TO 3070
3030 NEXT u
3040 REM DONE SCANNING MESSAGES
3045 GO SUB nl: GO SUB nl
3050 LET p$="message scan completed...": GO SUB sm: PAUSE 100: GO TO inputcom
3060 REM MESSAGE #X FOUND
3070 GO SUB cls: LET p$="Message #"+STR$ u: GO SUB sm: GO SUB nl
3080 LET p$=m$(u): GO SUB sm
3085 GO SUB nl: GO SUB nl: LET p$="N=Next One D=Delete Q=Quit: ": GO SUB sm
3090 GO SUB getkey: LET c$=r$
3095 IF c$="N" OR c$="n" THEN GO SUB cls: GO TO 3030
3100 IF c$="D" OR c$="d" THEN FOR x=1 TO 400: LET m$(u,x)="": NEXT x: GO SUB nl: LET p$="*DELETED*": LET m(u)=0: GO SUB sm: PAUSE 100: GO SUB cls: RETURN
3105 IF c$="Q" OR c$="q" THEN GO TO inputcom
3110 GO TO 3090
4000 REM leave message
4010 LET y=1
4015 IF m(y)=0 THEN GO TO 4025
4020 LET y=y+1: GO TO 4015
4025 GO SUB cls
4030 LET p$="Message #"+STR$ y: GO SUB sm
4032 GO SUB nl: GO SUB nl
4035 LET p$="Who gets message?: ": GO SUB sm
4040 GO SUB getkey: LET a$=r$
4042 LET i$="": LET k$=""
4045 FOR x=1 TO LEN a$
4050 IF a$(x)<>" " THEN LET i$=i$+a$(x)
4055 IF a$(x)=" " THEN GO TO 4065
4060 NEXT x
4065 LET k$=a$(x+1 TO )
4066 FOR x=1 TO tu: IF f$(x,1 TO LEN i$)=i$ AND l$(x,1 TO LEN k$)=k$ THEN GO TO 4070
4067 NEXT x
4070 LET w(x)=w(x)+1
4072 LET m$(y,1 TO 32)="To: "+a$
4075 LET p$="From: "+u$: GO SUB sm: LET m$(y,33 TO 64)=p$
4080 GO SUB nl
4085 LET p$="Input message (400 chars max)...<CTRL C> to save...": GO SUB sm: GO SUB nl
4090 LET wm=1: GO SUB getkey: LET wm=0: LET p$="*DONE*": GO SUB sm: LET m$(y,65 TO )=r$
4095 LET m$(y,LEN r$+65)=CHR$ 3
4110 GO SUB nl: GO SUB nl
4120 LET p$="R=Redo S=Save A=Abort: (R,S,A)": GO SUB sm
4125 GO SUB getkey: LET c$=r$
4130 IF c$="R" OR c$="r" THEN GO TO 4025
4135 IF c$="S" OR c$="s" THEN LET m(y)=1: LET p$="*STORED*": GO SUB sm: PAUSE 100: GO TO inputcom
4140 IF c$="A" OR c$="a" THEN GO TO inputcom
4145 GO TO 4125
5000 REM quit message base
5001 ON ERR RESET
5100 GO SUB nl
5110 LET p$="Thank you for calling!"
5115 GO SUB cls
5120 GO SUB sm
5125 OUT 119,64: OUT 119,0: OUT 119,0
5130 PAUSE 100: GO TO autoans
6000 REM LOGON MESSAGE
6001 PAUSE 30: FOR x=1 TO 30: RANDOMIZE USR output: OUT 115,0: NEXT x
6002 IF IN 119<128 THEN GO TO quit
6008 RANDOMIZE USR output: OUT 115,28
6010 GO SUB cls
6020 LET P$=" Willie's TS2068 BBS c/o Indiana Sinclair-Timex User Group": GO SUB sm
6030 FOR x=1 TO 5: GO SUB nl: NEXT x
6040 RETURN
6100 REM Utilities Sub-Menu
6102 GO SUB cls
6105 DATA " Utility Sub-Menu","<C>hange Personal UserFile","<L>ist of users","<R>eturn to Main Menu"
6110 RESTORE 6105
6115 FOR p=1 TO 4: READ p$: GO SUB sm: GO SUB nl: NEXT p
6117 GO SUB nl: GO SUB nl: LET p$="Choice? (C,L, or R): ": GO SUB sm
6120 GO SUB getkey
6125 IF r$="C" OR r$="c" THEN GO TO 6200
6130 IF r$="L" OR r$="l" THEN GO TO 6300
6135 IF r$="R" OR r$="r" THEN GO TO inputcom
6140 GO TO 6120
6199 REM CHANGE user file
6200 GO SUB cls
6205 LET p$="First Name: "+f$(un): GO SUB sm
6207 IF LEN f$(un)<20 THEN GO SUB nl
6210 LET p$="Last Name : "+l$(un): GO SUB sm
6213 IF LEN l$(un)<20 THEN GO SUB nl
6215 LET p$="PASSWORD : "+w$(un): GO SUB sm
6220 GO SUB nl
6225 LET p$="Change line? (1-3, 0=Exit): ": GO SUB sm: GO SUB getkey
6227 LET c$=r$
6230 IF c$="0" THEN GO TO 6100
6235 IF c$="3" THEN LET p$="New Password: ": GO SUB sm: GO SUB getkey: IF LEN r$>8 THEN LET p$="Must be 1-8 characters!": GO SUB sm: GO TO 6235
6237 IF c$="3" THEN LET w$(un)=r$
6240 IF c$="2" THEN LET p$="New Last Name: ": GO SUB sm: GO SUB getkey: LET l$(un)=r$
6245 IF c$="1" THEN LET p$="New First Name: ": GO SUB sm: GO SUB getkey: LET f$(un)=r$
6250 GO TO 6200
6300 REM transmit userfile list
6305 GO SUB cls
6310 LET p$="Press ENTER when ready:": GO SUB sm
6315 LET c=USR input
6316 IF c<>13 THEN GO TO 6315
6317 GO SUB nl
6320 FOR x=1 TO tu
6322 LET p$="User File #"+STR$ x
6323 GO SUB sm: GO SUB nl
6325 LET p$="Last name: "+l$(x): GO SUB sm
6327 IF LEN l$(x)<20 THEN GO SUB nl
6330 LET p$="First name: "+f$(x): GO SUB sm
6335 GO SUB nl: NEXT x
6340 GO SUB nl: LET p$="Press any key to go to main menu": GO SUB sm: LET c=USR input: GO TO inputcom
7000 REM chat
7001 ON ERR GO TO quit
7005 GO SUB nl: LET p$="Paging sysop...................": FOR x=1 TO 10: BEEP .1,1: NEXT x: GO SUB sm: GO SUB nl
7006 FOR x=1 TO 128: IF INKEY$="" THEN LET p$=".": GO SUB sm: BEEP .1,10: NEXT x: LET p$="The sysop is not available.....": GO TO inputcom
7008 GO SUB nl: LET p$="You are now in chat mode:": GO SUB sm: GO SUB nl
7009 CLS : PRINT "You are in chat mode with ";u$: PRINT : PRINT "SYMBL SHIFT/NOT to escape..."
7015 POKE 23692,255
7020 LET RxRDY=0: LET TxRDY=0
7100 LET a=USR statchk: LET TxRDY=1 AND (a=1 OR a=3): LET RxRDY=1 AND (a=2 OR a=3)
7130 IF RxRDY THEN LET k=USR input: PRINT CHR$ k;: IF k=13 THEN PRINT ">";
7135 IF RxRDY THEN IF k=3 THEN GO TO inputcom
7140 IF TxRDY AND INKEY$<>"" THEN GO SUB 7160
7150 GO TO 7010
7160 IF TxRDY THEN LET i$=INKEY$: PRINT i$;: IF CODE i$<>12 THEN OUT 115,CODE i$: IF CODE i$=13 THEN PRINT ">";
7161 IF CODE i$=195 THEN GO TO inputcom
7162 IF INKEY$<>"" THEN GO TO 7162
7163 RETURN
7200 FOR d=1 TO LEN p$: RANDOMIZE USR output: OUT 115,CODE p$(d): PRINT p$(d);
7202 IF CODE p$(d)=3 THEN GO TO 7210
7205 NEXT d
7210 RETURN
7220 REM Send a CHR$ 13
7230 POKE 23692,255: PRINT : RANDOMIZE USR output: OUT 115,13: RETURN
7235 REM input subroutine
7237 LET r$=""
7240 LET c=USR input
7241 IF c=3 AND wm=1 THEN RETURN
7242 IF c=13 AND wm=1 THEN LET r$=r$+CHR$ 13: GO TO 7240
7245 IF c=13 THEN LET wm=0: RETURN
7250 IF c=8 THEN LET r$=r$( TO (LEN r$-(LEN r$>0))): GO TO 7240
7255 IF c<>13 AND c<>8 AND (c<32 OR c>122) THEN GO TO 7240
7260 LET r$=r$+CHR$ c: PRINT CHR$ c;
7265 GO TO 7240
7400 CLS : RANDOMIZE USR output: OUT 115,28: RANDOMIZE USR output: OUT 115,31: RETURN
8000 REM user input subroutine
8010 LET p$="First Name: ": GO SUB sm
8020 GO SUB getkey: LET a$=r$
8050 LET p$="Last Name : ": GO SUB sm
8060 GO SUB getkey: LET b$=r$
8065 GO SUB nl: LET p$="Above correct? (Y/N): ": GO SUB sm: GO SUB getkey: IF r$="n" OR r$="N" THEN GO TO 8000
8075 LET u$=a$+" "+b$
8080 FOR x=1 TO tu
8085 IF l$(x,1 TO LEN b$)=b$ AND f$(x,1 TO LEN a$)=a$ THEN GO TO 8300
8090 NEXT x
8120 LET p$="You are a new user....": GO SUB sm
8125 REM new user
8127 LET tu=tu+1: IF tu>50 THEN LET tu=tu-1: LET p$="Sorry! Total number of users allowed access to this system has been reached. Please call back in a few days, by then a new spot should be open: THANKS!"
8128 GO SUB sm: PAUSE 300: GO TO quit
8130 GO SUB cls
8132 LET un=x
8135 LET p$=" WELCOME!": GO SUB sm: GO SUB nl: GO SUB nl
8140 GO SUB nl: GO SUB nl
8145 LET p$="I do hope you enjoy your visit with my BBS and please feel freeto leave any comments you wish addressed to SYSOP": GO SUB sm
8150 GO SUB nl: GO SUB nl
8155 LET p$="Please enter a 1-8 character password for further protection of your private mail-": GO SUB sm
8160 GO SUB nl
8165 GO SUB nl: LET p$="PASSWORD: ": GO SUB sm
8170 GO SUB getkey: LET c$=r$: IF LEN c$>8 THEN LET p$="Must be 1-8 characters ONLY!": GO SUB sm: GO TO 8165
8175 LET p$="Your password is "+c$: GO SUB sm: LET w$(un)=c$
8180 GO SUB nl: LET p$="Change password? (Y/N):": GO SUB sm
8185 GO SUB getkey
8190 IF r$="Y" OR r$="y" THEN GO TO 8160
8195 LET l$(un)=b$: LET f$(un)=a$
8200 LET z$(un)=d$
8205 LET t(un)=1
8250 PAUSE 150: GO TO inputcom
8300 REM old user
8305 GO SUB nl
8310 LET un=x
8315 LET p$="PASSWORD : ": GO SUB sm
8320 GO SUB getkey
8407 GO SUB nl
8410 IF r$<>w$(un,1 TO LEN r$) THEN LET p$="Incorrect password -- Try again": GO SUB sm: GO TO 8300
8415 LET p$="You have called this system "+STR$ t(un): GO SUB sm: GO SUB nl: LET p$="times before.": GO SUB sm
8417 GO SUB nl
8420 LET p$="Last time called was on "+z$(un): GO SUB sm
8425 IF w(un)>0 THEN LET p$="You have "+STR$ w(un)+" messages waiting.": GO SUB sm
8430 LET z$(un)=d$
8435 LET t(un)=t(un)+1
8440 PAUSE 200: GO TO inputcom
9990 REM timer
9992 DIM x$(5): LET time=INT ((PEEK 23672+256*PEEK 23673+256*256*PEEK 23674)/60): LET hr=INT (time/60/60): LET min=INT ((time-(hr*60*60))/60): PRINT AT 20,0;"0" AND hr<10;hr;":";"0" AND min<10;min: FOR x=1 TO 5: LET x$(x)=SCREEN$ (20,x-1): NEXT x
9993 RETURN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.