--- title: "2068 BBS Program" id: 59981 type: "computer_media" slug: "2068-bbs-program" url: "http://localhost/computer_media/2068-bbs-program/" markdown_url: "http://localhost/computer_media/2068-bbs-program.md" published_at: "2025-06-14T12:05:09+00:00" modified_at: "2026-04-03T07:58:00+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2019/02/20230809-041332.jpg" excerpt: "A fully functional modem-driven bulletin board system with message storage, multi-mode reading, sysop chat, and embedded machine code serial I/O — all in BASIC." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "BBS" slug: "bbs" taxonomy: "post_tag" url: "http://localhost/tag/bbs/" - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" indiv: - name: "Lucy Gordon" slug: "lucy-gordon" taxonomy: "indiv" url: "http://localhost/indiv/lucy-gordon/" - name: "Phillip Basford" slug: "phillip-basford" taxonomy: "indiv" url: "http://localhost/indiv/phillip-basford/" - name: "Randy Gordon" slug: "randy-gordon" taxonomy: "indiv" url: "http://localhost/indiv/randy-gordon/" genre: - name: "BBS" slug: "bbs" taxonomy: "genre" url: "http://localhost/type/bbs/" media_type: "Program" programmers: - name: "Phillip Basford" slug: "phillip-basford" taxonomy: "indiv" url: "http://localhost/indiv/phillip-basford/" - name: "Randy Gordon" slug: "randy-gordon" taxonomy: "indiv" url: "http://localhost/indiv/randy-gordon/" - name: "Lucy Gordon" slug: "lucy-gordon" taxonomy: "indiv" url: "http://localhost/indiv/lucy-gordon/" download_url: "https://archive.org/download/timex-sinclair-software-archive/2068%20BBS%20Program.zip" mediadate: "1986" images: - url: "http://localhost/wp-content/uploads/2025/06/2068-bbs.png" article_media: - id: 36191 title: "2068 BBS Program" type: "article" url: "http://localhost/article/2068-bbs-program/" media_type_tags: "BBS" --- 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 `l` holds 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,255` resets the scroll count to prevent the “scroll?” prompt from interrupting serial output. - Line 9201 uses the TS2068-specific `ON ERR RESET` keyword to clear the error state before the disconnect sequence. - Data persistence uses `SAVE "" DATA m$()` and `SAVE "" DATA y()` to tape, with a corresponding LOAD path at lines 20–23. ### Bugs and Anomalies - **Line 2340:**`LET y2=ys+c1` — variable `ys` is undefined; should be `y2=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 variable `in` is tested (lines 2310, 2320) rather than being set by the input subroutine — `in` is 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 be `LET p$=l$( TO a)` or `m$(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<123` is logically always true for any value of `in` and should likely be `in>=32 AND in<123` to filter printable ASCII. ## 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 ```