Extended Color BASIC to Arduino Sketch, part 4

See also: Part 1Part 2, Part 3, Part 4 and full source.

Put on your protective eyewear, folks. You are about to see something very, very disturbing. So disturbing, in fact, that I am hesitant to share it with you. But I will, anyway.

A few nights ago, I began the process of converting my 1983 BBS program line-by-line from Microsoft Extended Color Basic over to Arduino C. This isn’t a port or a rewrite. It’s a conversion as close as possible to the original BASIC source. And it’s something that should never be done.

So let’s get started, and do it.

I will be including the original BASIC source code as comments, followed by the lines of C that replicate the commands. Anything that was a GOSUB will be turned in to a function, but everything else will just be lines of C code, with liberal use of labels and the C “goto” command. (And if anyone ever asks you if it is possible to use goto in C, tell them no. You do not believe it is. We can stop this insanity before it spreads any further.)

Variables

In BASIC, all variables are global and accessible anywhere, so for this conversion, all (almost) of the variables will be made global as well.

In BASIC, there are two types of variables – strings, and numeric. A string is represented by the name of the variable followed by a dollar sign. For instance, A$ or NM$. A numeric variable has no dollar sign, like X or Y.

While BASIC would let you have long variable names, only the first two characters were processed, so a variable called “NAME$” would really be “NA$”. A clever programmer could make sure that the first two letters were unique, and use long variable names like “NAME” and “USERNUMBER” to make the code easier to read. But since I needed every last byte of memory I could get, I did not waste the space on longer variable names.

In BASIC, you could have a string variable called NM$ and a numeric variable called NM. In C, variable names must be unique. You couldn’t have both “int x;” and “char x;”. Because of this, I chose to rename all the basic string variables and add “Str” to the end of the name. I also converted variable names to lowercase, which wasn’t really necessary, but I was already breaking enough rules without having uppercase variable names. NM$ in BASIC becomes nmStr in C. Numeric variables remained the same, just in lowercase.

In BASIC, strings are dynamic. You can have A$=”HELLO” and B$=”GOODBYE” and create a new string like C$=A$+B$. Standard C does not work this way, so all string variables had to have their maximum size predefined. The *ALL RAM* BBS was written with certain maximum sizes in mind for user names and message lines, so I chose to use those values for the C string lengths.

Memory

On the original TRS-80 Color Computer, I had 32K of memory to play with. On the Arduino UNO R3, I had less than 2K. In BASIC, and with most C systems, all code and initialized variables (like strings) were stored in the same place. But, the Arduino uses something called Harvard Architecture, where the contents of Flash are separate from the RAM. A normal C program that has any string constants (char *msg=”Hello…”) will have those strings loaded in to RAM, taking up some of the precious 2K. For tiny small programs, this is no big deal. but for this project, every last byte is needed.

There are methods to use the Flash for string storage on the Arduino and I had to learn about them and make extensive use of them. This may be unfamiliar to most C programmers, but we can discuss the techniques I used to save memory in a future article. For now, just know there were some things I had to do for this that would not have been needed if there was more RAM to play with.

Due to the very limited amount of memory, I had to greatly downsize the in-memory storage. Instead of holding 200 users, I might only be able to support three or four. Instead of twenty messages of ten lines each, I might only be able to support three tiny messages of, say, two lines each. And instead of each line being up to 64 characters (twice the screen width of the Color Computer’s screen), I changed that to just 32. This makes Twitter posts look huge by comparison.

And, since I needed a quick way to test the limits, I decided to replace all hard coded values in the BASIC source with #defines in the C version. This let me easily change the maximum number of users or the message size to see how far I could get.

Printing

In BASIC, the PRINT command either prints with a carriage return at the end, or not, depending on the use of a semicolon. PRINT”HELLO” would have a carraige return, but PRINT”HELLO”; would not. Originally, I put in the appropriate Arduino “Serial.println()” or “Serial.print()”, but once it came time to deal with TAB, I had to rethink that. In order to track tabs, I needed my own print routine that could keep track of how many characters had been displayed since the last carriage return.

I decided to create print() and printSemi() to act like PRINT and PRINT;. I also created printTab(), printComma() and even a scaled down version of PRINT USING called printUsing(). They do not quite look the same but they should get the same results

But, that was not enough. When BASIC prints a numeric variable, it puts a space before and after the number. So I created printNum() and printNumSemi(). And due to how Arduino handles printing out different variable types, I had to create a special routine for when I wanted to print a single character (else my print routine would print it as a number).

I also had to deal with the different types of strings the Arduino has — whether variables in RAM, or strings from Flash. It was quite a learning experience, and I will document this, as well, in a future article.

GOTO

Yes, C has a goto, but don’t tell anyone you hear it from me. A label is made (“label:”) and then you simply “goto label;”. There are much better ways to do this in C, but since this is a literal translation, I used goto. So there.

BASIC Commands

BASIC has many functions that do not exist in C, so I created some workalike C functions. They are not full implementations of the ones found in Microsoft BASIC — they were just enough to replicate the functionality needed for the BBS.

I even created versions of the SOUND and CLS commands, even though this Arduino version has no sounds or video screen. Line by line port, remember?

Source Code

And now… the source code. This version is trimmed down a bit, with extra things (like support for Ethernet and SD card storage), removed so we can just focus on the conversion. I will post the full source later.

#include 

void setup()
{
  Serial.begin(9600);
  while(!Serial);
  allram();
}

void loop()
{
}

/*---------------------------------------------------------------------------*/
// In BASIC, strings are dynamic. For C, we have to pre-allocate buffers for
// the strings.
#define INPUT_SIZE  32  // 64. For aStr, etc.

#define MAX_USERS   3   // NM$(200) Userlog size. (0-200, 0 is Sysop)
#define NAME_SIZE   10  // 20. Username size (nmStr)
#define PSWD_SIZE   8   // 8. Password size (psStr & pwStr) 
#define ULOG_SIZE   (NAME_SIZE+1+PSWD_SIZE+1+1)

// To avoid hard coding some values, we define these here, too. Each message
// is made up of lines, and the first line will contain the From, To, and
// Subject separated by a character. So, while the original BASIC version
// hard coded this, we will calculate it, letting the subject be as large
// as whatever is left over (plus room for separaters and NULL at the end).
#define FR_SIZE     NAME_SIZE                      // From
#define TO_SIZE     NAME_SIZE                      // To
#define SB_SIZE     (INPUT_SIZE-FR_SIZE-1-TO_SIZE) // "FromToSubj"

// The original BASIC version was hard-coded to hold 20 messages of 11 lines
// each (the first line was used for From/To/Subject). The Arduino has far
// less RAM, so these have been made #defines so they can be changed.
#define MAX_MSGS    4   // 19  (0-19, 20 messages)
#define MAX_LINE    2   // 10  (0-10, 11 lines)

// Rough estimate of how many bytes these items will take up.
#define ULOG_MEM    ((MAX_USERS+1)*(ULOG_SIZE))
#define MBASE_MEM   ((MAX_MSGS+1)*MAX_LINE*INPUT_SIZE)

// Validate the settings before compiling.
#if (FR_SIZE+1+TO_SIZE+SB+SIZE > INPUT_SIZE)
#error INPUT_SIZE too small to hold "FromToSub".
#endif

/*---------------------------------------------------------------------------*/

/// And now... The BASIC code begins.

//0 REM *ALL RAM* BBS System 1.0
//1 REM   Shareware / (C) 1983
//2 REM     By Allen Huffman
//3 REM  110 Champions Dr, #811
//4 REM     Lufkin, TX 75901
//5 CLS:FORA=0TO8:READA$:POKE1024+A,VAL("&H"+A$):NEXTA:EXEC1024:DATAC6,1,96,BC,1F,2,7E,96,A3

*** We will do a CLS later. This FOR/NEXT loop was to load a bit of assembly language in to memory and execute it. This routine would clear memory and give the maximum amount to BASIC. And no, I haven’t lived there since 1995.

*** Next, we have the variables. You didn’t have to pre-define them in BASIC, but it would speed things up later when they were first used since memory would be set aside ahead of time. In the original, sizes were hard coded (see the 200, 19, and 10, below) but in the C code we will use the #define values defined earlier.

//10 CLEAR21000:DIMNM$(200),MS$(19,10),A$,F$,S$,T$,BR$,CL$,NM$,PS$,PW$,A,B,C,CL,LN,LV,MS,NM,KY,UC

// All variables in BASIC are global, so we are declaring them outside the
// functions to make them global in C as well. Arrays in BASIC are "0 to X",
// and in C they are "0 to X-1", so we add one to them in C to get the same
// number of elements.
char nmArray[MAX_USERS+1][ULOG_SIZE];             // NM$(200)
char msArray[MAX_MSGS+1][MAX_LINE+1][INPUT_SIZE]; // MS$(19,10)
char aStr[INPUT_SIZE];                            // A$
char fStr[FR_SIZE];                               // F$ - From
char sStr[SB_SIZE];                               // S$ - Subj
char tStr[TO_SIZE];                               // T$ - To
char nmStr[NAME_SIZE];                            // NM$ - Name
char psStr[PSWD_SIZE];                            // PS$ - Pswd
char pwStr[PSWD_SIZE];                            // PW$ - Pswd

// To save RAM, these two strings will exist in Flash memory. It will
// require a bit of work later to use them (__FlashStringHelper*).
prog_char brStr[] PROGMEM = "*==============*==============*"; // BR$ - border
prog_char clStr[] PROGMEM = "x0cx0e";                        // CL$ - clear

*** Above, PROGMEM causes these strings to be stored in Flash. You will see later that it also requires special code to access them.

int a, b, c, cl, ln, lv, ms, nm, ky, uc;
// A, B, C - misc.
// CL - Calls
// LN - Line Number
// LV - Level
// MS - Messages
// NM - Names (users)
// KY - Keys (commands entered)
// UC - Uppercase input (1=Yes, 0=No)

*** Above, I chose int as the variable type, but byte would be better since none of my numeric variables ever go negative. I should change this, and save an extra 10 bytes (trust me, it all mattered in this project). The only exception would be the CL number of calls variable, since a byte could only count to 255. But, this isn’t a real useful BBS, so beyond a few test calls, maybe that would be fine.

*** The next function is the main BBS code. The only thing not inside of it are routines that were GOSUBS, and any additional helper functions to replicate certain BASIC commands.


void allram()
{
  // HACK - create adefault Sysop account.
  nm = 0;
  strncpy_P(nmArray[0], PSTR("SYSOP\\TEST9"), ULOG_SIZE);

*** The original BASIC version required there to be a User 0 for the system operator account. It could never run without it, since that user could not be deleted. But, since we are not using the editor to create users, I hard coded a SysOp account.

  cls(); // From line 5  

  //15 CL$=CHR$(12)+CHR$(14):BR$="*==============*==============*":GOSUB555
  //char cl[] = "\0xC\0xE";
  //char br[] = "*==============*==============*";
  gosub555();

line20:
  //20 CLS:PRINTTAB(6)"*ALL RAM* BBS SYSTEM":PRINT"USERS:"NM,"CALLS:"CL:PRINTTAB(5)"SYSTEM AWAITING CALLER";:GOSUB1005:SOUND200,10
  cls();
  printTab(6);
  print(F("*ALL RAM* BBS SYSTEM"));
  printSemi(F("USERS:"));
  printSemi(nm);
  printComma();
  printSemi(F("CALLS:"));
  print(cl);
  printTab(5);
  printSemi(F("SYSTEM AWAITING CALLER"));
  gosub1005();
  sound(200,10);

*** As you can see, the print() routines look a bit different, but you should be able to follow them exactly as the original BASIC code. The sound() call doesn’t do anything, but I did try to make it delay the same amount of time the CoCo’s sound would have played.

  //25 A$="Welcome To *ALL RAM* BBS!":GOSUB1055:KY=0:CL=CL+1
  strncpy_P(aStr, PSTR("Welcome To *ALL RAM* BBS!"), INPUT_SIZE);
  gosub1055();
  ky = 0;
  cl = cl + 1;

*** In BASIC, you could assign a string just by setting it to a variable. I figured the closest version of this in C would be the string copy function. So, A$=”whatever” becomes a call to copy “whatever” in to the aStr string. The PSTR() macro causes the string to be stored in Flash, and the special version of strcpy_P() is used to copy from Flash to RAM. The end result is the same.

  //30 PRINT:PRINT"Password or 'NEW' :";:UC=1:GOSUB1005:PS$=A$:IFA$=""ORA$="NEW"THEN55ELSEPRINT"Checking: ";:A=0
line30:
  print();
  printSemi(F("Password or 'NEW' :"));
  uc = 1;
  gosub1005();
  strncpy(psStr, aStr, PSWD_SIZE);
  if (aStr[0]=='\0' || strcmp(aStr, "NEW")==0)
  {
    goto line55;
  }
  else
  {
    printSemi(F("Checking: "));
    a = 0;
  }

*** In my original BASIC version, the function starting at line 1005 was an input routine. It would read a line from the user in to A$. It would use the variable UC to know if it should return the string in ALL UPPERCASE. Even at such a young age, I had already figured out parameter passing… I just didn’t have a programming language that supported it.

line35:
  //35 A$=NM$(A):B=INSTR(A$,""):NM$=LEFT$(A$,B-1):PW$=MID$(A$,B+1,LEN(A$)-B-1):LV=VAL(RIGHT$(A$,1)):IFPW$=PS$THEN45ELSEA=A+1:IFA< =NM THEN35
  strncpy(aStr, nmArray[a], ULOG_SIZE);
  b = instr(aStr, "\\");
  strncpy(nmStr, aStr, b-1);
  nmStr[b-1] = '\0';  
  strncpy(pwStr, &aStr[b], strlen(aStr)-b-1);
  pwStr[strlen(aStr)-b-1] = '\0';
  lv = atoi(&aStr[strlen(aStr)-1]);
  if (strncmp(pwStr, psStr, PSWD_SIZE)==0)
  {
    goto line45;
  }
  else
  {
    a = a + 1;
    if (a<=nm) goto line35;
  }

*** I wrote my own INSTR() routines to replicate the BASIC command. Instead of comparing a string with IF A$=B$, the C string compare strcmp() was used.


line40: // for empty userlog bug
  //40 PRINT"*INVALID*":KY=KY+1:IFKY20THEN30
  print();
  printSemi(F("Full Name :"));
  uc = 1;
  gosub1005();
  strncpy(nmStr, aStr, NAME_SIZE);
  if (aStr[0]=='\0' || strlen(aStr)>20) goto line30;

  //70 PRINT"Password  :";:UC=1:GOSUB1005:PW$=A$:IFA$=""ORLEN(A$)>8THEN30
  printSemi(F("Password  :"));
  uc = 1;
  gosub1005();
  strncpy(pwStr, aStr, PSWD_SIZE);
  if (aStr[0]=='\0' || strlen(aStr)>8) goto line30;

  //75 PRINT:PRINT"Name :"NM$:PRINT"Pswd :"PW$:PRINT"Is this correct? ";:UC=1:GOSUB1005:IFLEFT$(A$,1)="Y"THEN80ELSE65
  print();
  printSemi(F("Name :"));
  print(nmStr);
  printSemi(F("Pswd :"));
  print(pwStr);
  printSemi(F("Is this correct? "));
  uc = 1;
  gosub1005();
  if (aStr[0]=='Y')
  {
    goto line80;
  }
  else
  {
    goto line65;
  }

*** I could have written something to handle LEFT$, but since I only use it to look at the first character of a string, I decided to just do it the C way and look at the first character of the string.

line80:
  //80 NM=NM+1:NM$(NM)=NM$+""+PW$+"0":LV=0:KY=0
  nm = nm + 1;
  strncpy(nmArray[nm], nmStr, NAME_SIZE);
  strcat_P(nmArray[nm], PSTR("\\"));
  strncat(nmArray[nm], pwStr, PSWD_SIZE);
  //strcat_P(nmArray[nm], PSTR("0"));
  strcat_P(nmArray[nm], PSTR("1")); // AUTO VALIDATED
  //lv = 0;
  lv = 1;
  ky = 0;
  //85 PRINT"Your password will be validated as soon as time permits.  Press":PRINT"[ENTER] to continue :";:GOSUB1005
  print(F("Your password will be validated as soon as time permits.  Press"));
  printSemi(F("[ENTER] to continue :"));
  gosub1005();

*** BASIC allows building strings just by adding them up. I used the C string concatanate strcat() routine to do the same. I have some lines commented out, because the original made all new users unvalidated, and for testing, I wanted someone to be able to register and actually use the system.

  //100 'Main Menu
line105:
  //105 A$="*ALL RAM* BBS Master Menu":GOSUB1055
  strncpy_P(aStr, PSTR("*ALL RAM* BBS Master Menu"), INPUT_SIZE);
  gosub1055();

  //110 PRINT"C-all Sysop","P-ost Msg":PRINT"G-oodbye","R-ead Msg":PRINT"U-serlog","S-can Titles"
  printSemi(F("C-all Sysop"));
  printComma();
  print(F("P-ost Msg"));
  printSemi(F("G-oodbye"));
  printComma();
  print(F("R-ead Msg"));
  printSemi(F("U-serlog"));
  printComma();
  print(F("S-can Titles"));

line115:
  //115 PRINTBR$
  print((__FlashStringHelper*)brStr);

*** The weird casting to __FlashStringHelper* is something needed to let the print() routine handle string variables that are stored in Flash.

line120:
  //120 KY=KY+1:IFKY>200THENPRINT"Sorry, your time on-line is up.":GOTO210ELSEIFKY>180THENPRINT"Please complete your call soon."
  ky = ky + 1;
  if (ky>200)
  {
    print(F("Sorry, your time on-line is up."));
    goto line210;
  }
  else if (ky>180)
  {
    print(F("Please complete your call soon."));
  }

line125:
  //125 PRINTTAB(7)"?=Menu/Command :";:UC=1:GOSUB1005:A$=LEFT$(A$,1)
  printTab(7);
  printSemi(F("?=Menu/Command :"));
  uc = 1;
  gosub1005();
  aStr[1] = '\0';

line130:
  //130 LN=INSTR("?CGRSPU%",A$):IFLN=0THENPRINT"*Invalid Command*":GOTO120
  ln = instr("?CGRSPU%", aStr);
  if (ln==0)
  {
    print(F("*Invalid Command*"));
    goto line120;
  }

  //135 IFLV5THENPRINT" Sorry, you are not validated.":GOTO125
  if (lv5)
  {
    print(F(" Sorry, you are not validated."));
    goto line125;
  }

  //140 ONLN GOTO105,155,205,405,455,305,255,505
  if (ln==1) goto line105;
  if (ln==2) goto line155;
  if (ln==3) goto line205;
  if (ln==4) goto line405;
  if (ln==5) goto line455;
  if (ln==6) goto line305;
  if (ln==7) goto line255;
  if (ln==8) goto line505;

*** A C switch()/case might have been closer to ON GOTO, but I just chose to do it the brute force way.

  //150 'Call Sysop
line155:
  //155 A$="Calling the Sysop":GOSUB1055:A=0
  strncpy_P(aStr, PSTR("Calling the Sysop"), INPUT_SIZE);
  gosub1055();
  a = 0;

*** Annoying. I should have known it was “SysOp” and not “Sysop” back then. It stands for System Operator.

  //165 PRINT" BEEP!";:SOUND150,5:IFINKEY$=CHR$(12)THEN175ELSEprintING$(5,8);:A=A+1:IFA";A;:GOSUB1005:MS$(MS,A)=A$:IFA$=""THENA=A-1:GOTO345ELSEIFA", a);
  gosub1005();
  strncpy(msArray[ms][a], aStr, INPUT_SIZE);
  if (aStr[0]=='\0')
  {
    a = a - 1;
    goto line345;
  }
  else if (a</pre>
*** Since I did not create a LEFT$ function, I cheat and just hack the original aStr to be 2 characters long, by placing a NULL terminator character in position three. C strings are base-0, so it is [0][1][2].
<pre>
  //365 PRINT"Line currently reads:":PRINTMS$(MS,LN):PRINT"Enter new line:":GOSUB1005:A$=LEFT$(A$,64):IFA$=""THENPRINT"*Unchanged*"ELSEMS$(MS,LN)=A$:PRINT"*Corrected*"
  print(F("Line currently reads:"));
  print(msArray[ms][ln]);
  print(F("Enter new line:"));
  gosub1005();
  aStr[INPUT_SIZE-1] = '\0';
  if (aStr[0]=='\0')
  {
    print(F("*Unchanged*"));
  }
  else
  {
    strncpy(msArray[ms][ln], aStr, INPUT_SIZE);
    print(F("*Corrected*"));
  }

*** You will see that wherever I need to shorten a string using LEFT$, I just do the trick of NULL terminating it at that position. The -1 is because the C string is base-0, but BASIC counts the string characters starting at 1. I think.

  //370 GOTO360
  goto line360;

line375:
  //375 CLS:PRINTCL$"Message Reads:":FORB=1TOA:PRINTUSING"##>";B;:PRINTMS$(MS,B):NEXTB:GOTO345
  cls();
  print((__FlashStringHelper*)clStr);
  print(F("Message Reads:"));
  for (b=1; b< =a; b++) { printUsingSemi("##>", b);
    print(msArray[ms][b]);
  }
  goto line345;

*** PRINT USING in BASIC could do a bunch of things, but I was only using it to print a number without spaces before and after it, which is what happens in BASIC if you just print a numeric variable.

line380:
  //380 MS$(MS,0)=T$+""+F$+""+S$:MS=MS+1:PRINT"*Message"MS"stored*":GOTO115
  strcpy(msArray[ms][0], tStr);
  strcat_P(msArray[ms][0], PSTR("\\"));
  strcat(msArray[ms][0], fStr);
  strcat_P(msArray[ms][0], PSTR("\\"));
  strcat(msArray[ms][0], sStr);
  ms = ms + 1;
  printSemi(F("*Message"));
  printSemi(ms);
  print(F("stored*"));
  goto line115;

line385:
  //385 PRINT"*Message Aborted*":GOTO115
  print(F("*Message Aborted*"));
  goto line115;

  //400 'Read Msg
line405:
  //405 IFMS=0THENPRINT"The message base is empty.":GOTO115
  if (ms==0)
  {
    print(F("The message base is empty."));
    goto line115;
  }

  //410 CLS:PRINTCL$
  cls();
  print((__FlashStringHelper*)clStr);

line415:
  //415 PRINT"Read Message 1 -"MS":";:GOSUB1005:A=VAL(LEFT$(A$,2)):IFAMS THEN115
  printSemi(F("Read Message 1 -"));
  printSemi(ms);
  printSemi(F(":"));
  gosub1005();
  aStr[2] = '\0';
  a = atoi(aStr);
  if (ams) goto line115;

  //420 A$=MS$(A-1,0):B=INSTR(A$,""):C=INSTR(B+1,A$,""):T$=LEFT$(A$,B-1):F$=MID$(A$,B+1,C-B-1):S$=RIGHT$(A$,LEN(A$)-C)
  strncpy(aStr, msArray[a-1][0], INPUT_SIZE);
  b = instr(aStr, "\\");
  c = instr(b+1, aStr, "\\");
  strncpy(tStr, aStr, b-1);
  tStr[b-1] = '\0';
  strncpy(fStr, (aStr-1)+b+1, c-b-1);
  fStr[c-b-1] = '\0'; // FIXTHIS - max copy sizes here?
  strncpy(sStr, right(aStr, strlen(aStr)-c), SB_SIZE);

  //425 IFS$="*E-Mail*"ANDLV<8THENIFNM$<>T$ANDNM$<>F$THENPRINT"That message is private.":GOTO415
  if (strcmp(sStr, "*E-Mail*")==0 && lv<8)
  {
    if (strcmp(nmStr, tStr)!=0 && strcmp(nmStr, fStr)!=0)
    {
      print(F("That message is private."));
      goto line415;
    }
  }

  //430 CLS:PRINTCL$"Message #"A:PRINT"From :"F$:PRINT"To   :"T$:PRINT"Subj :"S$:PRINT:B=0
  cls();
  print((__FlashStringHelper*)clStr);
  printSemi(F("Message #"));
  print(a);
  printSemi(F("From :"));
  print(fStr);
  printSemi(F("To   :"));
  print(tStr);
  printSemi(F("Subj :"));
  print(sStr);
  print();
  b = 0;

line435:
  //435 B=B+1:PRINTMS$(A-1,B):IFMS$(A-1,B)=""THEN440ELSEIFB"?DROWSSAP"THENPRINT"Thank You!":GOTO115
  printSemi(F("PASSWORD?"));
  gosub1005();
  if (strcmp(aStr, "?DROWSSAP")!=0)
  {
    print(F("Thank You!"));
    goto line115;
  }

*** And now you know the super secret shutdown password.

  //515 PRINT"Abort BBS? YES or NO? ";:UC=1:GOSUB1005:IFA$<>"YES"THEN115
  printSemi(F("Abort BBS? YES or NO? "));
  uc = 1;
  gosub1005();
  if (strcmp(aStr, "YES")!=0) goto line115;

  //520 GOSUB605:STOP
  gosub605();
  return;
} // end of allram()

*** Next we have the subroutines, which were originally GOSUBs. Since I was not able to hook up a cassette recorder to my Arduino, I chose to just fake the load and save routines.


/*---------------------------------------------------------------------------*/
// Subroutines (formerly GOSUBs)
//
//550 '%LOAD%
void gosub555()
{
  //555 PRINT"%LOAD% [ENTER] WHEN READY";:GOSUB1005
  printSemi(F("%LOAD% [ENTER] WHEN READY"));
  //  gosub1005();
  print();
  if (aStr[0]=='!') return;

  //560 OPEN"I",#-1,"USERLOG":INPUT#-1,CL,NM:FORA=0TONM:INPUT#-1,NM$(A):NEXTA:CLOSE
  loadUserlog();

  //565 OPEN"I",#-1,"MSG BASE":INPUT#-1,MS:FORA=0TOMS-1:FORB=0TO10:INPUT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:RETURN
  loadMsgBase();
}

//600 '%SAVE%
void gosub605()
{
  //605 PRINT"%SAVE% [ENTER] WHEN READY";:GOSUB1005:MOTORON:FORA=0TO999:NEXTA
  printSemi(F("%SAVE% [ENTER] WHEN READY"));
  gosub1005();

  //610 OPEN"O",#-1,"USERLOG":PRINT#-1,CL,NM:FORA=0TONM:PRINT#-1,NM$(A):NEXTA:CLOSE
  saveUserlog();

  //615 OPEN"O",#-1,"MSG BASE":PRINT#-1,MS:FORA=0TOMS-1:FORB=0TO10:PRINT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:RETURN
  saveMsgBase();
}

//1000 'User Input
#define CR         13
#define INBUF_SIZE 64
void gosub1005()
{
  byte ch; // Used only here, so we can make it local.

  //1005 LINEINPUTA$:A$=LEFT$(A$,64):IFUC=0ORA$=""THENRETURN
  lineinput(aStr, INPUT_SIZE);
  if ((uc==0) || (aStr[0]=='\0')) return;

  //1010 FORC=1TOLEN(A$):CH=ASC(MID$(A$,C,1)):IFCH>96THENMID$(A$,C,1)=CHR$(CH-32)
  for (c=0; c96) aStr = ch-32;    
    //1015 IFCH=92THENMID$(A$,C,1)="/"
    if (ch==92) aStr = '/';
    //1020 NEXTC:UC=0:RETURN
  }
  uc = 0;
}

//1050 'Function Border
void gosub1055()
{
  //1055 CLS:PRINTCL$BR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$:RETURN
  cls();
  print((__FlashStringHelper*)clStr);
  print((__FlashStringHelper*)brStr);
  printTab((32-strlen(aStr))/2);
  print(aStr);
  print((__FlashStringHelper*)brStr);
}

*** Now we try to recreate some of the BASIC commands I used. Or at least just enough of them to get the desired results.


/*---------------------------------------------------------------------------*/
// The following functions mimic some of the Extended Color BASIC commands.

// CLS
// Clear the screen.
void cls()
{
  print(F("n--------------------------------n"));
}

// SOUND tone, duration
// On the CoCo, tone (1-255), duration (1-255; 15=1 second).
void sound(byte tone, byte duration)
{
  Serial.write(0x07);   // BEL
  delay(duration*66.6); // Estimated delay.
}

/*---------------------------------------------------------------------------*/
// String functions.

// STRING$(length, charcode)
// Generate a string of length charcode chracters.
void string(byte length, byte charcode)
{
  int i;

  for (i=0; i0)
    {
      ch = Serial.read();
    }
    else
    {
      continue; // No data. Go back to the while()...
    }
    switch(ch)
    {
    case -1: // No data available.
      break;

    case CR:
      print();
      cmdLine[cmdLen] = '\0';
      done = true;
      break;

    case BS:
      if (cmdLen>0)
      {
        printCharSemi(BS);
        printSemi(F(" "));
        printCharSemi(BS);
        cmdLen--;
      }
      break;

    default:
      // If there is room, store any printable characters in the cmdline.
      if (cmdLen31) && (ch<127)) // isprint(ch) does not work.
        {
          printCharSemi(ch);
          cmdLine[cmdLen] = ch; //toupper(ch);
          cmdLen++;
        }
      }
      else
      {
        printCharSemi(BEL); // Overflow. Ring 'dat bell.
      }
      break;
    } // end of switch(ch)
  } // end of while(!done)

  return cmdLen;
}

// INKEY$
// Return character waiting (if any) from standard input (not ethernet).
char inkey()
{
  if (Serial.available()==0) return 0;
  return Serial.read();
}

/*---------------------------------------------------------------------------*/

// File I/O
// Ideally, I would have created wrappers for the OPEN, READ, CLOSE commands,
// but I was in a hurry, so...

//560 OPEN"I",#-1,"USERLOG":INPUT#-1,CL,NM:FORA=0TONM:INPUT#-1,NM$(A):NEXTA:CLOSE
void loadUserlog()
{
  print(F("(USERLOG would be loaded from tape here.)"));
}

//565 OPEN"I",#-1,"MSG BASE":INPUT#-1,MS:FORA=0TOMS-1:FORB=0TO10:INPUT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:RETURN
void loadMsgBase()
{
  print(F("(MSGBASE would be loaded from tape here.)"));
}

//610 OPEN"O",#-1,"USERLOG":PRINT#-1,CL,NM:FORA=0TONM:PRINT#-1,NM$(A):NEXTA:CLOSE
void saveUserlog()
{
  print(F("save USERLOG"));
}

//615 OPEN"O",#-1,"MSG BASE":PRINT#-1,MS:FORA=0TOMS-1:FORB=0TO10:PRINT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:RETURN
void saveMsgBase()
{
  print(F("save MSGBASE"));
}

*** Next, the PRINT routines. Due to the way things work with all these Flash based strings, I had to duplicate some of these routines with the only difference being what type of parameter was passed in (RAM string versus Flash string). This was the first time I really made use of any C++ functionality.

/*---------------------------------------------------------------------------*/
// Print (output) routines.

// For TAB to work, we need to track where we think we are on the line.
byte tabPos = 0;

// We want to simulate the following:
// PRINT "HELLO"  -- string, with carraige return at end of line
// PRINT A        -- number (space before and after), with carraige return
// PRINT "HELLO"; -- no carraige return
// PRINT TAB(5);  -- tab to position 5
// PRINT "A","B"  -- tab to column 16 (on 32-column screen)

// Due to various types of strings on Arduino (in memory or Flash), we will
// have to duplicate some functions to have versions that take the other
// types of strings.

// printTypeSemi() routines will not put a carraige return at the end.

// PRINT TAB(column);
// PRINT TAB(column);
void printTab(byte column)
{
  while(tabPos10)
      {
        tempNum = tempNum/10;
        numDigits++;
      }
      while(numDigits</pre>
And there you have it, for better or for worse. I will follow up with a full posting of all the source, including the Ethernet and SD card routines (but there's not enough memory to use them both at the same time).

More to come...

3 thoughts on “Extended Color BASIC to Arduino Sketch, part 4

  1. Pingback: 1983 *ALL RAM* BBS ported to Arduino from BASIC | Appleause

  2. Pingback: 1983 *ALL RAM* BBS ported to Arduino from BASIC | Sub-Etha Software

  3. Pingback: Extended Color BASIC to Arduino Sketch, part 3 | Sub-Etha Software

Leave a Reply