A complete bulletin board system (BBS) for the TS2068. Supports up to 90 stored messages with forward and reverse browsing, message entry with automatic word wrap, direct message retrieval by number, and a real-time sysop chat mode. Uses machine code for serial I/O and stores the message base to tape between sessions.
What it does
This is a fully functional BBS (Bulletin Board System) for the TS2068, designed to run over a serial/modem connection. It POKEs 56 bytes of machine code into high memory (address 65480) at startup to handle low-level RS-232 serial I/O via ports 115 and 119. It stores up to 90 messages in a string array (m$(90,300)), each up to 300 characters, and can save/load the message base to tape.
Once a caller connects, the BBS handles modem handshaking, prompts for a username, then presents a menu with six options: forward or reverse reading through messages, leaving a message (with word wrap), reading a message by number, and a sysop chat mode that pages the operator with an ascending beep sequence. The time-on-system is calculated from the TS2068’s system clock registers and displayed in the menu. Disconnection is handled gracefully with a hangup sequence on port 119.
Program Analysis
Program Structure
The program is organized into clearly separated functional blocks:
- Lines 10–25: Initialization, machine code installation, array setup, and optional data load.
- Lines 1000–1090: Modem handshake, carrier detect, main menu dispatch loop.
- Lines 2100–2218: Reverse and forward message reading routines.
- Lines 2300–2350: Leave message, with word-wrap logic.
- Lines 2400–2418: Read message by number.
- Lines 2500–2596: Real-time sysop chat mode.
- Lines 8000–8024: String output subroutines (menu text, prompts).
- Lines 9000–9950: Utility routines: save program, serial transmit loop, disconnect, input handler, message reader, time formatter, and data save.
Machine Code Installation
Lines 14–16 install 56 bytes of machine code at address 65480. The code is stored as a string of packed three-digit decimal values in l$, and a FOR loop POKEs each byte using VAL l$(3*x+1 TO 3*x+3). The entry points are stored in named variables for clarity:
| Variable | Address | Purpose |
|---|---|---|
o | 65523 | Serial transmit USR entry |
i | 65489 | Serial receive USR entry |
l | 9100 | BASIC line number for string-send loop |
aa | 1000 | Main loop restart target |
q | 9200 | Error/disconnect handler target |
Serial I/O uses ports 115 (data) and 119 (control/status), consistent with the TS2068’s built-in RS-232 interface. The variable l is used as a numeric GO SUB target (line 9100), allowing the string-send loop to be called via GO SUB l — a compact idiom that avoids a literal line number in each of the eight output subroutines.
Modem Handshake and Carrier Detect
Lines 1000–1020 implement the modem answer sequence. Port 119 is written with control bytes to initialize the serial port, and the program then polls IN 119 waiting for a carrier signal (line 1004 loops while the status byte equals 5). Lines 1010–1020 send modem command bytes and perform a short handshake delay with PAUSE 300, followed by 30 iterations of the machine code transmit routine to flush or synchronize the line. The receive loop at lines 1018–1020 checks the high bit of the status byte to detect data readiness.
Input Handler (Line 9400)
The main character input routine at line 9400 accumulates characters into l$ one at a time. It uses ON ERR GO TO q to trap serial errors (e.g., loss of carrier) and redirect to the disconnect/goodbye handler. Characters below 32 or above 122 are discarded, and lowercase letters are converted to uppercase by subtracting 32 from their code. The routine returns after a single character, requiring the caller to loop (e.g., lines 9406–9412). The received byte is stored in in via PEEK 65479 after the machine code routine deposits it there.
Message Storage
Messages are stored in m$(90,300), a 90-row by 300-character string array. Each message record is structured as a header block prepended at write time (line 2312): "TO: " + recipient + CHR$ 13 + "FROM: " + caller_name + CHR$ 13. A CHR$ 7 (BEL) sentinel is written at the end of the message body to mark its actual length. The read routine at lines 9500–9510 scans backwards from position 300 to find this sentinel, then passes only the valid prefix to the output routine.
Word-Wrap Logic
Lines 2330–2346 implement a simple word-wrap algorithm for messages longer than 32 characters. Starting from position 33, the code scans left to find a space character, replaces it with CHR$ 13 (carriage return), then copies that segment into the message array and recurses on the remainder. There is a notable bug on line 2340: LET y2=ys+c1 references the variable ys, which is never defined anywhere in the program; this should almost certainly be y2=y2+c1, meaning word-wrapped messages will likely store with a corrupted offset.
Chat Mode
Lines 2500–2596 implement a duplex chat mode. The sysop is paged with a beep sequence (line 2506 ramps pitch over 128 iterations). If the sysop presses a key, full-duplex chat begins. The machine code at address 65480 is polled via LET a=USR 65480 (line 2562), which returns a bitmask: 1 = transmit ready, 2 = receive data present, 3 = both. Incoming characters are printed with a “>” line-break on CR; outgoing characters are sent with OUT 115, CODE l$ with a short FOR delay loop (line 2592). The escape condition checks for CODE INKEY$=195, which is the BASIC keyword token for a specific key combination rather than a standard ASCII code.
Time-On Display
Subroutine 9900 reads the TS2068 frame counter from system variables at addresses 23672, 23673, and 23674, combining them as t = PEEK 23672 + 2^8*PEEK 23673 + 2^16*PEEK 23674. The counter is reset to zero at line 1012 when a caller connects, so elapsed time in seconds (at 50 frames/second) is computed and formatted as m:ss for display in the main menu. This is a standard technique for elapsed-time tracking without a real-time clock.
Notable Techniques
VAL "number"is used in several GO TO and GO SUB targets (e.g.,GO TO VAL "1002",GO SUB VAL "8002") as a memory-saving optimization — the tokenized line number takes more space than a short string.- The variable
lholds the numeric value 9100 and is used directly as a GO SUB target:GO SUB l. This is legal in Sinclair BASIC and avoids repeating the literal line number across many output subroutines. POKE 23692,255resets the scroll count to prevent the “scroll?” prompt from interrupting serial output.- Line 9201 uses the TS2068-specific
ON ERR RESETkeyword to clear the error state before the disconnect sequence. - Data persistence uses
SAVE "" DATA m$()andSAVE "" DATA y()to tape, with a corresponding LOAD path at lines 20–23.
Bugs and Anomalies
- Line 2340:
LET y2=ys+c1— variableysis undefined; should bey2=y2+c1. This will corrupt message write offsets whenever word-wrap is triggered. - Line 2506: The label says
p$=l$(LEN l$)is checked at line 1028 for CHR$ 13 termination, but in section 2300 the variableinis tested (lines 2310, 2320) rather than being set by the input subroutine —inis only populated inside the machine code receive path, so these checks may not behave as intended from a BASIC-only perspective. - Line 9506:
LET p$=( TO a)is missing the array reference — should beLET p$=l$( TO a)orm$(b)( TO a). As written, this is a syntax error or will reference an unexpected variable. - Line 2576: The condition
IF in>32 or in<123is logically always true for any value ofinand should likely bein>=32 AND in<123to filter printable ASCII.
Content
Source Code
10 PAPER 0: INK 7: BORDER 1: CLS
12 PRINT "stop tape"'''"then press (ENTER)": PAUSE 0: CLEAR 65478
14 LET l$="175219119230003079006000201219119230128200175219119230002040244219116050199255079219119230128200175219119230001040244121211115201219119230128200175219119230001040240201"
16 FOR x=0 TO 55: POKE 65480+x,VAL l$(3*x+1 to 3*x+3): NEXT x
18 LET o=VAL"65523": LET i=val "65489": LET l=val "9100": LET aa=val "1000": LET q=val "9200":
20 DIM m$(90,300): DIM y(1): PRINT "load?": PAUSE 0: IF INKEY$<>"y" THEN GO TO 24
22 LOAD "" DATA m$(): LOAD "" DATA y(): LET y1=y(1)
23 GO TO 25
24 LET y=10
1000 OUT 119,34: OUT 119,0
1001 POKE 23692,255: CLS: PRINT """2068 BBS"""
1002 LET x=IN 119
1004 IF x=5 THEN GO TO VAL "1002"
1010 OUT 119,2: OUT 119,34: PAUSE 300: OUT 119,64: OUT 119,123: OUT 119,55
1012 POKE 23674,0: POKE 23673,0: POKE 23672,0
1014 PAUSE 120
1016 FOR x=1 TO 30: RANDOMIZE USR o: OUT 115,0: NEXT X
1018 LET a=IN 119: IF a<128 THEN GO TO 1020
1020 RANDOMIZE USR o: OUT 115,28: RANDOMIZE USR o: OUT 115,31: RANDOMIZE USR o: OUT 115,28
1021 LET x=IN 115
1022 GO SUB VAL "8002"
1024 GO SUB 9400
1026 GO SUB 9405
1028 IF CODE l$(LEN l$)<>13 THEN GO TO 1026
1030 CLS: PRINT l$; " calling": LET u$=l$
1032 GO SUB 8006
1034 GO SUB 9400
1038 IF l$="R" THEN GO TO 2100
1040 IF l$="F" THEN GO TO 2200
1042 IF l$="L" THEN GO TO 2300
1044 IF l$="B" THEN GO TO 9200
1046 IF l$="#" THEN GO TO 2400
1048 IF l$="C" THEN GO TO 2500
1090 GO TO 1032
2100 REM Reverse Read
2104 FOR b=y1 TO 1 STEP -1
2105 GO SUB 8018
2106 GO SUB 9500
2114 IF l$="N" THEN NEXT b
2116 IF l$="M" THEN GO TO 1032
2118 GO TO 1032
2200 REM Forward Read
2204 FOR b=1 to y1
2205 GO SUB 8018
2206 GO SUB 9500
2214 IF l$="N" THEN NEXT b
2216 IF l$="M" THEN GO TO 1032
2218 GO TO 1032
2300 REM Leave Message
2302 LET y1=y1+1: IF y1=90 THEN LET y1=1
2304 GO SUB 8010
2306 GO SUB 9400
2308 GO SUB 9405
2310 IF in<>13 THEN GO TO 2308
2312 LET l$="TO: "+l$+CHR$ 13+"FROM: "+u$+CHR$ 13: LET y2=LEN l$: LET m$(y1, TO y2)=l$
2314 GO SUB 8012
2316 GO SUB 9400
2318 GO SUB 9402
2320 IF in<>13 THEN GO TO 2318
2322 REM Wordwrap
2330 IF LEN l$>299-y2+1 THEN LET l$=l$( TO 299-y2)
2332 LET c1=33
2334 IF l$(c1)<>" " THEN LET c1=c1-1
2336 IF l$(c1)=" " THEN LET l$(c1)=CHR$ 13
2338 IF CODE l$(c1)<>13 THEN GO TO 2334
2340 LET m$(y1,y2+1 TO y2+c1)=l$( TO C1): LET y2=ys+c1
2342 LET l$=l$(c1+1 TO )
2344 IF LEN l$>32 THEN GO TO 2332
2345 LET l$=l$+CHR$ 7
2346 LET m$(y1,y2+1 TO 300)=l$
2350 GO TO 1032
2400 REM Read by Number
2401 GO SUB VAL "8016"
2402 GO SUB 9400
2404 GO SUB 9405
2406 IF in<>13 THEN GO TO 2404
2408 LET b=VAL l$( TO (LEN l$-1))
2410 GO SUB 8018
2412 GO SUB 9500
2414 IF l$="N" THEN GO TO VAL "2400"
2416 IF l$="M" THEN GO TO VAL "1032"
2418 GO TO 1032
2500 REM Chat Mode
2502 GO SUB 8020
2504 FOR x=1 TO 128
2506 BEEP .1,10+INT (x/10)
2508 RANDOMIZE USR o
2510 OUT 115,46
2512 IF INKEY$<>"" THEN GO TO 2550
2514 NEXT x
2516 GO SUB 8022: GO TO 1032
2550 CLS: PRINT "chat w/";u$
2552 GO SUB 8024
2554 PRINT "NOT to escape"
2556 IF CODE INKEY$=195 THEN GO TO 1032
2558 POKE 23692,255
2560 LET r=0: LET xmit=0
2562 LET a=USR 65480
2564 LET xmit=1 and (a=1 or a=3)
2565 LET r=1 and (a=2 or a=3)
2569 IF r then GO TO 2576
2572 GO TO 2588
2576 RANDOMIZE USR i: LET in=PEEK 65479: IF in>32 or in<123 THEN PRINT CHR$ in;: IF in=13 THEN PRINT ">": GO TO 2556
2588 IF xmit and INKEY$<>"" THEN GO SUB 2592
2590 GO TO 2556
2592 IF xmit THEN LET l$=INKEY$: PRINT l$;: OUT 115,CODE l$: FOR x=1 TO 5: NEXT x: IF CODE l$=13 THEN PRINT ">";
2596 RETURN
8000 REM Strings Going Out
8002 LET p$=CHR$ 12+"TIMEX BOARD"+CHR$ 13+"TURN YOUR CR SUPPRESSOR OFF"+CHR$ 13+CHR$ 13+"YOUR NAME?"+CHR$ 13+">"+CHR$ 7: GO SUB l: RETURN
8006 GO SUB VAL "9900": LET p$=CHR$ VAL "12"+"(B)YE BYE"+CHR$ 13+"(L)EAVE MSG."+CHR$ 13+"(F)WD. READ"+CHR$ 13+"(R)EV. READ"+CHR$ 13+"(#) READ BY #"+CHR$ 13+"(C)HAT"+CHR$ 13+"TIME ON "+l$+CHR$ 13: GO SUB l: RETURN
8008 LET p$=CHR$ 12+" BYE-BYE": GO SUB l: RETURN
8010 LET p$=CHR$ VAL "12"+"WHO GETS MESSAGE?"+CHR$ 13: GO SUB l: RETURN
8012 LET p$=CHR$ 12+"250 CHARACTERS MAX"+CHR$ 13+"(ENTER) SAVES MESSAGE"+CHR$ 13: GO SUB l: RETURN
8014 LET p$=CHR$ 13+"(N)EXT MESSAGE OR (M)ENU"+CHR$ 13: GO SUB l: RETURN
8016 LET p$=CHR$ 13+"INPUT MESSAGE # "CHR$ 13+"[1-90] ->": GO SUB l: RETURN
8018 LET p$=CHR$ 13+"MESSAGE # "+STR$ B+CHR$ 13: GO SUB l: RETURN
8020 LET p$=CHR$ 12+"PAGING SYSOP.....": GO SUB l: RETURN
8022 LET p$="HE'S NOT HERE!": GO SUB l: RETURN
8024 LET p$="OK, LET'S TALK...": GO SUB l: RETURN
9000 CLEAR: SAVE "BBS" LINE 10: STOP
9100 FOR x=1 TO LEN P$: RANDOMIZE USR o: OUT 115,CODE p$(x): NEXT x: POKE 23692,255: PRINT p$: RETURN
9201 GO SUB 8008: ON ERR RESET: RANDOMIZE USR o: OUT 115,28: RANDOMIZE USR o: OUT 115,31: GO SUB l: OUT 119,64: OUT 119,0: OUT 119,0
9202 BEEP .2,10: BEEP .3,-20
9204 GO TO aa
9400 LET l$=""
9406 ON ERR GO TO q: RANDOMIZE USR i: LET in=PEEK 65479
9407 IF in=13 THEN GO TO 9409
9408 IF IN<32 or in>122 THEN GO TO 9406
9409 POKE 23692,255: PRINT CHR$ in;
9410 LET ca=in: IF in>96 AND in<123 THEN LET ca=in-32: LET in=ca
9412 LET l$=l$+CHR$ in
9414 RETURN
9500 LET l$=m$(b)
9504 FOR a=300 to 1 step -1: IF CODE l$(a)<>7 THEN NEXT a
9506 LET p$=( TO a)
9510 GO SUB l: GO SUB 8014: GO SUB 9400: RETURN
9900 LET l$="": LET t=PEEK 23672+2^8*PEEK 23673+2^16*PEEK 23674: LET m=INT (t/3600): LET s=INT ((t/3600-m)*60): LET l$=STR$ m+":"+("0" and s<10)+STR$ s: RETURN
9950 SAVE "Msgs" DATA m$(): LET y(1)=y1: BEEP .1,50: SAVE "count" DATA y(): STOP
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
