Monthly Archives: April 2013

Telnet is pretty cool.

Now processing most of the Telnet protocol!
Now processing most of the Telnet protocol!

A few evenings ago, I noticed a bunch of garbage coming on from an Ethernet connection on the Arduino when a new connection was made to the example server code. This garbage turns out to be part of the Telnet protocol. There are various escape sequences (some of which are quite large) that flow across the connection, and must be handled else they pollute the data stream.

I began writing some code to do this, and like many tangents I get on, it has led me down quite the rabbit hole of discovery. My first stop was this very helpful web page that explained a bit about how Telnet works:

http://www.softpanorama.net/Net/Application_layer/telnet.shtml

I then proceeded to read the RFC documents about Telnet, and learn more details:

http://www.faqs.org/rfcs/rfc854.html

RFC 854 covers the Telnet protocol. This document is from 1983, which is the year I was writing my *ALL RAM* BBS system which I recently ported to the Arduino. There are other RFCs that cover specific aspects of Telnet, such as all the various Options that can be set.

Initially, I created something very simple that just “ate” all the Telnet escape codes. This is small code, and should be used on any Arduino sketch that expects someone to be able to Telnet in to the device. I will finish it and post it here, or as a sample on the Arduino webpage in the Playground area.

I soon learned there were even more codes than what I first learned, so I went and found more complete references:

http://www.tcpipguide.com/free/t_TelnetOptionsandOptionNegotiation-2.htm

Even after that, I still have a few others I can generate (from the PuTTY terminal program) and see but I haven’t found good documentation on them yet. (I just know it has an option in PuTTY to send special sequences, and I went through all of them to make sure I was handling them correctly.) I have learned quite a bit about Telnet in the past few days. It’s pretty neat.

So, I am now working on two things. One is a simple EthernetServer filter that will take care of the most simple bits of Telnet. I will make it so that can be compiled out, so it’s just a “eat all the Telnet escape sequences” thing, for sketches that are very tight on space.

The bigger project is the seTelnetServer. It’s a klunky name, but it follows that naming conventions I used back when I was coding 6809 assembly language utilities for the OS-9 operating system. seTelnetServer is going to be a more complete Telnet implementation, with (conditionally compiled out) the ability to emit all the protocol debug information on the console (for those curious). I am planning on supporting some of the basic features I see various Telnet clients try to set — line versus character mode, echo and things like that. It will have hooks in the code where you can modify it and handle more if you need to. For instance, do you want to do something with the BRK sequence? Or Suspend?

I am packaging this together in to a very simple-to-use server sketch that might be as easy to use as this:

void setup()
{
telnetInit(23); // Initialize, listening on port 23
}

void loop()
{
Serial.print("Waiting for connection:");
while(!done)
{
telnetInput(buffer, 80); // Read up to 80 characters.
// do stuff...
telnetPrint("Hello, user!");
}
}

The “telnetInput()” routine would take care of listening for a new connection, if there wasn’t one, and then read input from the user (handling Telnet protocol). If they disconnect, it would return a code that could be used to detect that and reset.

I have a rough version of this working. I even added the ability (with my Ethernet library fixes) for it to receive other connections while it is handling the first one and inform them that “The system is in use. Please try back later.” And, there is even an “offline” mode, so if the operator decides to log in via serial console, it will disable the Ethernet (again, giving those who connect a “System is offline” message) while the operator is using it.

Sounds like fun. And when I am done, I plan to end up writing some Telnet software for the CoCo as well (though that has already been done for the DriveWire project).

More to come…

Multiple source files in an Arduino project

The Arduino IDE does an amazing job of hiding all the technical details of what it’s doing. This allows the some of the easiest creation of programs I have seen since the days of BASIC. From the looks of many of the forum questions over at the main Arduino website, it seems there are certainly a bunch of new programmers doing just this.

As an experience embedded programmer, much of what the Arduino IDE does seems to be magic to me. How can I just type a function without a prototype? How does it know what libraries to include when I don’t specify them? How is this able to work at all?

Over the past week, I have learned a bit more about what is going on behind the scenes. Apparently, the IDE has a preprocessor that converts the Arduino “Sketch” in to C++ code, generating prototypes and such automatically. I have already ran in to one problem with this.

Many other things remain a mystery, but at least one more has been explained today. I was very curious how one could split up a larger project in to multiple files. As it turns out, the Arduino IDE makes this super simple… Just make a new tab in the existing project.

In the Arduino 1.0.4 (current released version) editor, I noticed a square icon with a down arrow in it on the right side of the window, under the magnifying glass “search” button. I had seen this before, with “New Tab” and other options in it. I had assumed this was so you could switch between multiple projects in the same window, but now I understand this is how you have multiple files in the same project. Just create a tab, and put your code in it.

So, if I have my setup() and loop() in the main tab, and create a second tab with doSomething(), I can then call doSomething() from setup() or loop(). More magic.

I will be splitting up my various code snippets in to separate files for easy including in future projects.

I post this because I expect maybe I am not the only “experienced embedded programmer” who doesn’t read the manual.

A real Arduino Telnet server?

  • 2014/03/16 Update: The source code to this is now on GitHub. Check the Arduino link at the top of each page of this site.
This sketch tries to process Telnet protocol mesages.
This sketch tries to process Telnet protocol mesages.

The example code for the Ethernet library has some things that try to act like Telnet servers, but really all they do is open up a port and listen for data. When someone connects with a Telnet client, that client will send Telnet protocol messages trying to negotiate the connection. The Arduino examples I have found posted around do not deal with this, which means whatever “garbage” comes in could have unexpected results if the program isn’t properly ignoring invalid data.

Tonight, I began working on a more complete Telnet server for the Arduino. I am sure many of them exist, but what better way to learn than to reinvent the wheel?

I am doing a “fuller” version that would support many of the Telnet protocol options, then a dumber one that would just get rid of the protocol from the stream and ignore pretty much everything.

And, I will wrap that with a simple to use bit of code for making connections without all the tedious setup.

Comment if this project is of interest, as I have several others I may work on first.

Arduino compiler problem with #ifdefs solved.

In C, “#ifdef” or “#if defined()” are used to hide or include portions of code only if certain conditions are met. For example, my recent *ALL RAM* BBS experiment contains code for using the SD card library as well as the Ethernet library. I used #ifdef around specific blocks of code so I could compile versions with or without either of those libraries. But all is not well in Arduino land. Consider this following, simple example:

#if defined(FOO)
byte mac[] = { 0x2A, 0xA0, 0xD8, 0xFC, 0x8B, 0xEE };
#endif

void setup()
{
Serial.begin(9600);
while(!Serial);
Serial.println("Test...");
}

void loop()
{
}

This is supposed to only include the “byte mac[] =” line if “FOO” is defined, such as with:

#define FOO

However, on the current Arduino IDE (1.0.4), this simple code will fail with:

ifdef.ino: In function ‘void setup()’:
ifdef:18: error: ‘Serial’ was not declared in this scope

What? Suddenly “Serial.println()” won’t work? Moving the byte declaration outside of the #if def make it work. Very weird.

I also found a similar example, where I tried to comment out a function that used SD library variable types:

void setup()
{
Serial.begin(9600);
while(!Serial);
Serial.println("Test...");
}

void loop()
{
}

#ifdef FOO
byte fileReadln(File myFile, char *buffer, byte count)
{
}
#endif

In this example, I did not want the fileReadln() function to be included unless I had defined FOO. But, compiling this produces:

ifdef:15: error: ‘File’ was not declared in this scope
ifdef:15: error: expected primary-expression before ‘char’
ifdef:15: error: expected primary-expression before ‘count’
ifdef:15: error: initializer expression list treated as compound expression

Unhelpful. And after wasting some time on this, I started a topic in the Arduino.cc forums to ask if others were experiencing the same thing. And they were. A helpful post from parajew pointed me to this site which helped explain the problem, and offered a workaround:

http://www.a-control.de/arduino-fehler/?lang=en

The pre-processor does some stuff behind the scenes, creating prototypes and including header files where needed, and it just does it wrong. The A-Control site figured out a simple workaround, which I trimmed a bit to just adding this at the top of my scripts:

// BOF preprocessor bug prevent - insert me on top of your arduino-code
// From: http://www.a-control.de/arduino-fehler/?lang=en
#if 1
__asm volatile ("nop");
#endif

…and now either of my examples will compile as intended. Thank you, parajew and A-Control! I can now move on to my next problem…

Hope it helps you, too.

Arduino Ethernet and multiple socket server connections

Greetings! If you are finding this writeup useful, please leave a comment. I originally shared these modifications in April 8, 2013. In February 2015, I went through them again using the current Arduino 1.6.0. They still work, but I did tweak the source code notes a bit to be clearer. – Allen

  • 2014/04/03: I just started using GitHub and found all the Arduino sources there, including this bug report which seems to discuss and address this issue. I will be reviewing it when I have time to see if those fixes take care of this bug.
  • 2014/04/08: This is, by far, the most viewed article on this site. I suppose I should do more posts about Arduino Ethernet.
  • 2013/04/09: I have done some more tweaks to the code listed in this article, and will try to update them when I have a chance. I will probably spin it off in to a whole new article on an easy “drop in” telnet server I am working on.
  • 2014/04/14: Fixed some HTML escape codes in the source code. (Thanks, Matt!)
  • 2014/04/16: You can now get an Ethernet shield shipped from the US for $11.49.
  • 2014/04/19: In the comments, Petr Stehlík pointed out that it doesn’t look like a check against incoming IP address is done, meaning that if any packet was received with the same port, it would just be accepted. I will need to investigate the rest of the code and see if it does that check. If not, you could blast a packet to an Arduino and as long as you match the port, it would accept it. That seems real bad, but should be very easy to fix, if needed.
  • 2015/02/15: I just checked these updates against the Arduino 1.6.0 release, and they still work. I am updating the notes on this page to note where the libraries folder is found on Mac OS X, and to clarify where one of the changes goes. I have zipped up my changed and places them here: EthernetMultiServer.zip This may allow a clean 1.6.0 install to “Import Library” and work, but I have not tested that yet. Also, Arduino forum user SurferTim has contributed another way to accomplish this without fixing the library. He has posted a Telnet example in the Playground that talks directly to the Wiznet 5100 chip to keep the incoming connections straight. Very clever (and similar to what I had to do in the library to track the remote ports and keep them separated). Cool
  • 2015/05/22: Similar to the SurferTim approach, a comment by Gene provides another standalone way to do this in code without having to modify the library. He includes a full example in his comment.

NOTE: The links and prices given below may be out of date. Since then. I discovered this seller (kbellenterprises) on e-Bay. They offer some low-cost Arduino clone items. They have always been responsive, and ship very fast. They currently have an UNO clone for $8.49, and a Wiznet 5100 Ethernet shield for $11.49.

The Arduino Ethernet shield (or the $17.99 workalike made by SainSmart) adds internet support to the Arduino. The limited memory of the Arduino does not have to run a full TCP/IP stack. Instead, the Ethernet shield uses a Wizpro chip that handles Ethernet, TCP, UDP and IP protocols. This particular chip, Wizpro W5100, supports four simultaneous connections. This means you could have an Arduino sketch that opens four different websites at the same time, or you could run a server that allows up to four simultaneous users to connect.

At least, you could if the Ethernet library worked right. The way it was designed (I call it a bug or at least an oversight), the existing library would only allow four incoming connections if each one was listening on a different port. For example, if you have port 23 set up to monitor incoming TELNET connections, and one user was connected, any other attempts to connect would be refused until the first connection was closed. I wanted to use the “up to four connections” part to still allow other users to connect, and then tell them “The system is busy. Go away.”

My first attempt to do this was to just create two server instances:

EthernetServer server1(23);
EthernetServer server2(23);

I then modified the WebServer example and trimmed everything out except what I wanted. My main addition was this second server instance, and a check for connections to it while processing the primary connection. It looks like this (see notes afterwards):


// MultiServer Demo

#include <SPI.h>
#include <Ethernet.h>

byte      mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 0, 100);

EthernetServer server1(23);
EthernetServer server2(23);

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

// start the Ethernet connection and the server:
Ethernet.begin(mac, ip);
server1.begin();
server2.begin();
Serial.print("nServer is listening at ");
Serial.println(Ethernet.localIP());
}

void loop()
{
EthernetClient client2;

// listen for incoming clients
EthernetClient client1 = server1.available();
if (client1) {
Serial.println("Client A connected.");
client1.println("Greetings, program!");

while(client1.available()>0) client1.read(); // Gobble

// Loop while client is connected.
while (client1.connected())
{
client2 = server2.available();
if (client2.connected()) {
Serial.println("Client B connected. Getting rid of them...");
client2.println("nThe system is busy. Try back later.n");
delay(1);
client2.stop();
Serial.println("Client B disconnected.");
}
// Then handle the actual client.

// If data is available, just read it and write it back to the user.
if (client1.available())
{
char c = client1.read();
if (c>0) Serial.write(c);
}
} // end of while... go back and do it again.

// If here, we must no longer be connected.
delay(1);
// close the connection:
client1.stop();
Serial.println("Client A disconnected.");
}
}

My plans was to wait for a connection. Once I had one, I would sit in a loop (as long as they remained connected) and check a second server instance to see if an additional connection attempt was made. If a second attempt was made, I’d send them a quick status message, then shut them down and go back to monitoring the main connection.

This should work, but doesn’t. It produced unexpected results:

Server Output:

Serving at 10.0.0.42
Server is listening at 10.0.0.42
Client 1 connected.
Client 2 connected. Getting rid of them…
Client 2 disconnected.
Client 1 disconnected.

Client (telent) Output:

alsmb:~ allenh$ telnet 10.0.0.42
Trying 10.0.0.42…
Connected to 10.0.0.42.
Escape character is ‘^]’.
Greetings, program!
The system is busy. Try back later.
Connection closed by foreign host.
alsmb:~ allenh$

It seemed the Ethernet could not distinguish between the two connections. I altered the server config to use different ports:

EthernetServer server1(23);
EthernetServer server2(2323);

Perhaps I could let 23 (telnet) be the main port, and 2323 be the status port. This provided much better results.

Server Output:

Server is listening at 10.0.0.42
Client 1 connected.
hello (I typed this from telnet)
(then, I made a second connection from another terminal, to port 2323)
Client 2 connected. Getting rid of them…
Client 2 disconnected.

On the first terminal, I was able to type “hello” and continue my connection, while the second terminal connected, then received the “Go away” message (after pressing some keys to generate some data to wake up the Ethernet code).

So it could work, but not with the same port.

Thanks to a forum post I found earlier explaining how to obtain the remote connection’s IP address, I was aware that the source code to the Ethernet libraries was part of the Arduino IDE, and that it was easy to make changes.

I decided to take a look and see if I could figure out how this Wiznet chip works.

Wiznet W5100

The Wiznet chip is an independent device. The Arduino sends it commands via the SPI bus (like sending byte commands to it) and then data can be read and written back, similarly to talking to a serial port. The Wiznet chip can be programmed to listen to up to four different socket connections, and then it takes care of the rest. The Ardunio basically says “Hey, Wiznet… Listen for connections on Port 23 of Socket 0” and then the Arduino code will query the Wiznet chip saying “Is anyone there?” and if so, handle accordingly.

Inside the Ethernet library code, I saw that it had an array to hold the ports the user was configuring. When you do EthernetServer begin(23), it puts a 23 in the first available slot then programs the Wiznet chip accordingly. These slots are how the Arduino knows which socket of the Wiznet to query. And that is where the problem is.

If you do “EthernetServer server1(23)” followed by “server1.being()”, slot 0 is set up with 23 and Wiznet socket 0 is programmed to listen for port 23 connections. If you then do “EthernetServer server2(23)” followed by “server2.begin()”, then slot 1 is set up with 23 and Wiznet socket 1 is programed to listen to port 23 connections. The Wiznet hardware is fine even with all four of it’s sockets listening to the same port. It tracks the actual connection internally.

But the Arduino code ONLY tracks the port number. So, if someone connects to the first socket 0 port, and is using it, then someone tries to connect to port 23 again, the Wiznet will hook them up to socket 1. The Arduino code makes a mistake, and when it checks for data, it grabs the first slot that matches the desired port. So, it keeps reading and writing data to slot 0 (socket 0) and never sees the second port 23 connection.

To resolve this, I made a few minor changes to the Arduino ethernet library code. First, I added secondary storage to track four remote ports (the ports used on the connecting client), and then added a bit of code that walked through all the available sockets trying to match up local server port number AND remote client port…

And it worked the first time, much to my amazement!

Here are my notes and modifications. There are a few things I did which I am not certain are correct, but they worked so I am sharing them. I will make a note of the parts I am unclear on.

Ethernet Library Modifications:


/--------------------------------------------------------------------------/
// 2015-02-15: Verified against Arduino 1.6.0.
//
// To fix the Ethernet library so it correctly allows multiple connections
// to the same port, the following files will need to be modified:
//
// libraries/Ethernet/src/Ethernet.h
// libraries/Ethernet/src/Ethernet.cpp
//
// libraries/Ethernet/src/EthernetClient.cpp
// libraries/Ethernet/src/EthernetClient.h
//
// libraries/Ethernet/src/EthernetServer.cpp
//
// On Mac OS X, these are embedded inside the Arduino.app package. Browse
// to that in the Finder, then right-click and select "Show Package
// Contents" and then you can go to:
//
// Contents/Resources/Java/libraries
//
// Is there a better way?
/*

Modify the following files:

1) Ethernet.h: The Ethernet object currently only tracks which Port the
socket is listening to. Add the following array to hold the remote Port.

Add this after static "uint16_t _server_port[MAX_SOCK_NUM];"

// ACH - added
static uint16_t _client_port[MAX_SOCK_NUM]; // ACH

2) Ethernet.cpp: Add the declaraction of the new array.

Add this after "uint16_t EthernetClass::_server_port[MAX_SOCK_NUM]"

// ACH - added
uint16_t EthernetClass::_client_port[MAX_SOCK_NUM] = { 0, 0, 0, 0 }; // ACH

3) EthernetClient.h: Add prototypes for the new functions, and declare a
new local variable that will track the destination port of this client.

Add this in the private: section

// ACH - added
uint16_t _dstport; // ACH

// ACH - added
uint8_t *getRemoteIP(uint8_t remoteIP[]); // ACH
uint16_t getRemotePort(); // ACH

4)  EthernetClient.cpp: When the Client object is stopped, it resets the
_server_port to zero. We should probably do this for the new _client_port.
In void EthernetClient::stop():

Add after this: EthernetClass::_server_port[_sock] = 0;

// ACH - added
EthernetClass::_client_port[_sock] = 0; // ACH

Add these two functions at the bottom of the file:

// ACH - added
uint8_t *EthernetClient::getRemoteIP(uint8_t remoteIP[]) // ACH
{
W5100.readSnDIPR(_sock, remoteIP);
return remoteIP;
}

uint16_t EthernetClient::getRemotePort() // ACH
{
return W5100.readSnDPORT(_sock);
}

5) EthernetServer.cpp: This code has to be modified so when it checks for
a connection, it checks both the Port (existing code) AND the remote
client's port (new code). If the connection has never been made, it will
initialize the remote port varaible correctly.

Add the following code to available()

EthernetClient EthernetServer::available()
{
accept();

for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
(client.status() == SnSR::ESTABLISHED ||
client.status() == SnSR::CLOSE_WAIT)) {

// ACH - added
// See if we have identified this one before
if (EthernetClass::_client_port[sock] == 0 ) {
client._dstport = client.getRemotePort();
EthernetClass::_client_port[sock] = client._dstport;
return client;
}
if (EthernetClass::_client_port[sock] != client._dstport) {
// Not us!
continue;
}
// ACH - end of additions
//if (client.available()) { // ACH - comment out
// XXX: don't always pick the lowest numbered socket.
return client;
//} // ACH - comment out
}
}

return EthernetClient(MAX_SOCK_NUM);
}

...and code to write():

size_t EthernetServer::write(const uint8_t *buffer, size_t size)
{
size_t n = 0;

accept();

for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);

if (EthernetClass::_server_port[sock] == _port &&
// ACH - added
EthernetClass::_client_port[sock] == client._srcport && // ACH
client.status() == SnSR::ESTABLISHED) {
n += client.write(buffer, size);
}
}

return n;
}

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

During this research, I also found this part to be bothersome:


if (client.available()) {
// XXX: don't always pick the lowest numbered socket.
return client;
}

Unlike the EthernetClient.available(), the one for EthernetServer really shouldn’t be checking for data. It’s checking to see if a brand new connection has been made, then the code will be checking for data available elsewhere. For this to not just always return client would mean you would get a connection, but never see it until there is data. You couldn’t use this code to write a server that someone telnets to and it just spits out something (like a status or a time). It looks like it would force the remote user to send data first before the Arduino code even knows the connection is there.

I commented out the if so it always returns client, since it will bypass that if it finds no matching socket. It seems to work.

Bug?

Coming soon… Multi user BBS for Arduino!

1983 *ALL RAM* BBS ported to Arduino from BASIC

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

Updates:

  • 2/17/2017 – Removed link to my BBS since it hasn’t operated for years.
  • 6/24/2013 – Thanks to some comments, I fixed an issue around line 1010 where it was changing.
    aStr[ c]

    to aStr1. I am not sure why, but bracket-c-bracket must mean something to the code formatter I am using, which is unfortunate. Or maybe it’s just a bug. Which is also unfortunate. There was one other place in the code that had a bracket-c so I fixed it by adding a space after the bracket. Hopefully it works. Anyone know how to attach downloads to a WordPress posting?

  • 4/19/2013 – Sometimes, in the evening (or on weekends), I have my Arduino online. You can telnet to it: (no longer running)
  • 4/16/2013 – The WordPress editor just loves to mangle source code. I think I have finally gotten it all cleaned up. The version posted below is the latest version, with some bug fixes and other stuff cleaned up. Unlike the original posting, this one does not have Ethernet support. I have been working on a Telnet Server, and have been using the BBS to test it. You will see stuff #ifdef’d out below that makes use of the new Telnet functions. As soon as I have it polished, I will post it, too (or send it to you if you want to see the work-in-progress).
  • 7/1/2022 – Cleanup on aisle three…

Following up with part 1, part 2, part 3 and part 4, here is the source code. Remember, this is awful C. It is a line-by-line translation of BASIC to Arduino.

As I finish this experiment, I will continue to report the source code here to keep it in sync.

Source code last updated: 2/14/2015

See latest on my GitHub: GitHub – allenhuffman/ALLRAMBBS: 1983 cassette BBS ported from BASIC ;-)

// BOF preprocessor bug prevent - insert me on top of your arduino-code
#if 1
__asm volatile ("nop");
#endif
/*-----------------------------------------------------------------------------
 
 *ALL RAM* BBS for Arduino
 by Allen C. Huffman (alsplace@pobox.com / www.appleause.com)
 
 This is an experiment to see how easy it is to translate Microsoft BASIC to
 Arduino C. The translated C code is incredibly ugly and will be disturbing
 to most modern programmers. It was done just to see if it could be done, and
 what better thing to translate than a program that was also originally done
 just to see if it could be done -- a cassette based BBS package from 1983!
 
 About the *ALL RAM* BBS:
 
 The *ALL RAM* BBS System was writtin in 1983 for the Radio Shack TRS-80
 Color Computer ("CoCo"). At this time, existing BBS packages for the CoCo
 required "2-4 disk drives" to operate, so *ALL RAM* was created to prove
 a BBS could run from a cassette-based computer. Instead of using floppy
 disks to store the userlog and message base, they were contained entirely
 in RAM. The in-memory databases could be saved to cassette tape and
 reloaded later.
 
 The original BASIC source code is included in comments, followed by a very
 literal line-by-line translation to Arduino C. Remember, it's not a rewrite
 in C -- it's BASIC code done in C.
 
 Be warned. There be "gotos" ahead!
 
 References about memory saving:
 
 http://www.controllerprojects.com/2011/05/23/saving-ram-space-on-arduino-when-you-do-a-serial-printstring-in-quotes/
 http://www.adafruit.com/blog/2008/04/17/free-up-some-arduino-sram/
 
 2013-04-02 0.0 allenh - Initial version with working userlog.
 2013-04-03 1.0 allenh - Message base working. Core system fully functional.
 Preliminary support for Arduino Ethernet added.
 2013-04-04 1.1 allenh - SD card support for loading/saving.
 2013-04-05 1.2 allenh - Ethernet (telnet) support.
 2013-04-06 1.3 allenh - Cleanup for posting to www.appleause.com
 2013-04-09 1.4 allenh - Fixed a bug with INPUT_SIZE and input.
 2013-04-12 1.5 allenh - Integration with new Telnet server code, misc fixes.
 2015-02-14 1.6 allenh - Adding some "const" to make it build with 1.6.0.
 -----------------------------------------------------------------------------*/
#include <avr/pgmspace.h>

#define VERSION "1.6"

// To enable SD card support, define the PIN used by SD.begin(x)
//#define SD_PIN        4

// To enable Ethernet support, defined the PORT used by EthernetServer(PORT)
//#define ENET_PORT     23

// NOTE: On smaller Arduinos (UNO), there is not enough Flash or RAM to have
// both SD and Ethernet support at the same time.

#if defined(SD_PIN)
#include <SD.h>
#endif

#if defined(ENET_PORT)
#include <SPI.h>
#include <Ethernet.h>

extern EthernetClient client;

// Prototypes...
byte telnetRead(EthernetClient client);
byte telnetInput(EthernetClient client, char *cmdLine, byte len);
#endif

// And so it begins.
void setup()
{
  Serial.begin(9600);

  while(!Serial);

  showHeader();

#if defined(ENET_PORT)
  telnetInit();
#endif

  showConfig();

  // Scroll off any leftover console output.
  //for(int i=0; i<24; i++) print();
}

// We don't really have anything loop worthy for this program, so we'll
// just have some fun and pretend like we are starting up the CoCo each
// time through.
void loop()
{
  showCoCoHeader(); // Just for fun...

  allram();

  print();
  print(F("BREAK IN 520"));
  print(F("OK"));

  delay(5000);
}

// Show program header.
void showHeader()
{
  // Emit some startup stuff to the serial port.
  print(F("\n"
    "*ALL RAM* BBS for Arduino "VERSION" - 30 year anniversary edition!\n"
    "Copyright (C) 1983 by Allen C. Huffman\n"
    "Ported from TRS-80 Color Computer Extended Color BASIC.\n"
    "Build Date: "__DATE__" "__TIME__"\n"));
}

/*---------------------------------------------------------------------------*/
// 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   12   // 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) // "From\To\Subj"

// 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    3   // 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 "From\To\Sub".
#endif

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

//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
//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+1];// MS$(19,10) 1.4
char aStr[INPUT_SIZE+1];                          // A$ 1.4
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*).
const char PROGMEM brStr[] PROGMEM = "*==============*==============*"; // BR$ - border
const char PROGMEM clStr[] PROGMEM = "\x0c\x0e";                        // CL$ - clear

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)

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

  cls(); // From line 5  

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

//20 CLS:PRINTTAB(6)"*ALL RAM* BBS SYSTEM":PRINT"USERS:"NM,"CALLS:"CL:PRINTTAB(5)"SYSTEM AWAITING CALLER";:go:SOUND200,10
line20:
nmStr[0] = 0; // reset user.
  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();
  if (gosub1005()==255) goto line20;
  sound(200,10);

  //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;

  showLoginMessage();

  //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();
  if (gosub1005()==255) goto line20;
  strncpy(psStr, aStr, PSWD_SIZE);
  if (aStr[0]=='\0' || strcmp(aStr, "NEW")==0)
  {
    goto line55;
  }
  else
  {
    printSemi(F("Checking: "));
    a = 0;
  }

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;
  }

line40: // for empty userlog bug
  //40 PRINT"*INVALID*":KY=KY+1:IFKY<3THEN30ELSE215
  print(F("*INVALID*"));
  ky = ky + 1;
  if (ky<3) goto line30;
  goto line215;

line45:
  //45 PRINT"*ACCEPTED*":PRINTBR$:PRINT"On-Line: "NM$:PRINT"Access :"LV:PRINT"Caller :"CL:KY=0:GOTO115
  print(F("*ACCEPTED*"));
  print((__FlashStringHelper*)brStr);
  printSemi(F("On-Line: "));
  print(nmStr);
  printSemi(F("Access :"));
  print(lv);
  printSemi(F("Caller :"));
  print(cl);
  ky = 0;
  goto line115;

  //50 'New User
line55:
  //55 A$="Password Application Form":GOSUB1055
  strncpy_P(aStr, PSTR("Password Application Form"), INPUT_SIZE);
  gosub1055();

  //60 IFNM=200THENPRINT"Sorry, the userlog is full now.":GOTO215ELSEPRINT"Name=20 chars, Password=8 chars"
  if (nm==MAX_USERS)
  {
    print(F("Sorry, the userlog is full now."));
    goto line215;
  }
  else
  {
    printSemi(F("Name="));
    printNumSemi(NAME_SIZE);
    printSemi(F(" chars, Password="));
    printNumSemi(PSWD_SIZE); 
    printSemi(F(" chars"));
  }

line65:
  //65 PRINT:PRINT"Full Name :";:UC=1:GOSUB1005:NM$=A$:IFA$=""ORLEN(A$)>20THEN30
  print();
  printSemi(F("Full Name :"));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  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();
  if (gosub1005()==255) goto line20;
  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 (gosub1005()==255) goto line20;
  if (aStr[0]=='Y')
  {
    goto line80;
  }
  else
  {
    goto line65;
  }

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();
  if (gosub1005()==255) goto line20;

  //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);

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:
  showFreeRam();
  //125 PRINTTAB(7)"?=Menu/Command :";:UC=1:GOSUB1005:A$=LEFT$(A$,1)
  printTab(7);
  printSemi(F("?=Menu/Command :"));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  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 IFLV<1ANDLN>5THENPRINT" Sorry, you are not validated.":GOTO125
  if (lv<1 && ln>5)
  {
    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;

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

  //165 PRINT" BEEP!";:SOUND150,5:IFINKEY$=CHR$(12)THEN175ELSEprintING$(5,8);:A=A+1:IFA<25THEN165
line165:
  printSemi(F(" BEEP!"));
  sound(150, 5);
  if (inkey()==12)
  {
    goto line175;
  }
  else
  {
    string(5, 8);
    a = a + 1;
    if (a<25) goto line165;
  }

  //170 PRINT:PRINT" The Sysop is unavaliable now.":GOTO115
  print(F(""));
  print(F(" The Sysop is unavaliable now."));
  goto line115;

line175:
  //175 PRINT:PRINTTAB(6)"*Chat mode engaged*"
  print();
  printTab(6);
  print(F("*Chat mode engaged*"));

line180:
  //180 GOSUB1005:IFLEFT$(A$,3)="BYE"THEN185ELSE180
  //gosub1005();
  if (gosub1005()==255) goto line20;
  if (strncmp(aStr, "BYE", 3)==0)
  {
    goto line185;
  }
  else
  {
    goto line180;
  }

line185:
  //185 PRINTTAB(5)"*Chat mode terminated*":GOTO115
  goto line115;

  //200 'Goodbye
line205:
  //205 A$="Thank you for calling":GOSUB1055
  strncpy_P(aStr, PSTR("Thank you for calling"), INPUT_SIZE);
  gosub1055();

line210:
  //210 PRINT:PRINT"Goodbye, "NM$"!":PRINT:PRINT"Please call again."
  print(F(""));
  printSemi(F("Goodbye, "));
  printSemi(nmStr);
  print(F("!"));
  print();
  print(F("Please call again."));

line215:
  //215 PRINT:PRINT:PRINT"*ALL RAM* BBS disconnecting..."
  print();
  print();
  print(F("*ALL RAM* BBS disconnecting..."));

  //220 FORA=1TO1000:NEXTA
  delay(1000);

#if defined(ENET_PORT)
  telnetDisconnect();
#endif

  //225 GOTO20
  goto line20;

  //250 'Userlog
line255:
  //255 A$="List of Users":GOSUB1055:PRINT"Users on system:"NM:IFNM=0THEN115ELSEA=1
  strncpy_P(aStr, PSTR("List of Users"), INPUT_SIZE);
  gosub1055();
  printSemi(F("Users on system:"));
  print(nm);
  if (nm==0)
  {
    goto line115;
  }
  else
  {
    a = 1;
  }
line260:
  //260 A$=NM$(A):PRINTLEFT$(A$,INSTR(A$,"\")-1)TAB(29)RIGHT$(A$,1)
  strncpy(aStr, nmArray[a], INPUT_SIZE);
  {
    char tempStr[NAME_SIZE+1];         // Add room for NULL.
    strncpy(tempStr, aStr, NAME_SIZE+1);
    tempStr[instr(tempStr, "\\")-1] = '\0';
    printSemi(tempStr);
  }
  printTab(29);
  print(right(aStr,1));

  //265 IF(A/10)=INT(A/10)THENPRINT"C-ontinue or S-top :";:UC=1:GOSUB1005:IFLEFT$(A$,1)="S"THEN275
  if (a % 10 == 9)
  {
    printSemi(F("C-ontinue or S-top :"));
    uc = 1;
    //gosub1005();
    if (gosub1005()==255) goto line20;
    if (aStr[0]=='S') goto line275;
  }

  //270 A=A+1:IFA<=NM THEN260
  a = a + 1;
  if (a<=nm) goto line260;

line275:
  //275 PRINT"*End of Userlog*":GOTO115
  print(F("*End of Userlog*"));
  goto line115;

  //300 'Post Msg
line305:
  //305 IFMS=20THENPRINT"One moment, making room...":FORA=0TO18:FORB=0TO10:MS$(A,B)=MS$(A+1,B):NEXTB:NEXTA:MS=19
  if (ms==MAX_MSGS+1)
  {
    print(F("One moment, making room..."));
    for(a=0; a<=MAX_MSGS-1; a++)
    {
      for(b=0; b<=MAX_LINE; b++)
      {
        strncpy(msArray[a][b], msArray[a+1][b], INPUT_SIZE);
      }
    }
    ms = MAX_MSGS;
  }
  //310 CLS:PRINTCL$"This will be message #"MS+1:FORA=0TO10:MS$(MS,A)="":NEXTA:F$=NM$
  cls();
  print((__FlashStringHelper*)clStr);
  printSemi(F("This will be message #"));
  print(ms+1);
  for (a=0; a<=MAX_LINE; a++)
  {
    msArray[ms][a][0] = '\0';
  }
  strncpy(fStr, nmStr, NAME_SIZE);

  //315 PRINT"From :"F$:PRINT"To   :";:UC=1:GOSUB1005:A$=LEFT$(A$,20):T$=A$:IFA$=""THEN115
  printSemi(F("From :"));
  print(fStr);
  printSemi(F("To   :"));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  aStr[NAME_SIZE] = '\0';
  strncpy(tStr, aStr, TO_SIZE);
  if (aStr[0]=='\0') goto line115;

  //320 PRINT"Is this message private? ";:UC=1:GOSUB1005:IFLEFT$(A$,1)="Y"THENS$="*E-Mail*":GOTO330
  printSemi(F("Is this message private? "));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  if (aStr[0]=='Y')
  {
    strncpy_P(sStr, PSTR("*E-Mail*"), SB_SIZE);
    goto line330;
  }

  //325 PRINT"Subj :";:UC=1:GOSUB1005:A$=LEFT$(A$,18):S$=A$:IFA$=""THEN115
  printSemi(F("Subj :"));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  aStr[SB_SIZE-1] = '\0';
  strncpy(sStr, aStr, SB_SIZE);
  if (aStr[0]=='\0') goto line115;

line330:
  //330 PRINT"Enter up to 10 lines, 64 chars. [ENTER] on a blank line to end.":A=0
  printSemi(F("Enter up to"));
  printSemi(MAX_LINE);
  printSemi(F("lines,"));
  printSemi(INPUT_SIZE);
  print(F("chars. [ENTER] on a blank line to end."));
  a = 0;

line335:
  //335 A=A+1:PRINTUSING"##>";A;:GOSUB1005:MS$(MS,A)=A$:IFA$=""THENA=A-1:GOTO345ELSEIFA<10THEN335
  a = a + 1;
  printUsingSemi("##>", a);
  //gosub1005();
  if (gosub1005()==255) goto line20;
  strncpy(msArray[ms][a], aStr, INPUT_SIZE);
  if (aStr[0]=='\0')
  {
    a = a - 1;
    goto line345;
  }
  else if (a<MAX_LINE) goto line335;

line340:
  //340 PRINT"*Message Buffer Full*"
  print(F("*Message Buffer Full*"));

line345:
  //345 PRINT"A-bort, C-ont, E-dit, L-ist, or S-ave Message? ";:UC=1:GOSUB1005:A$=LEFT$(A$,1):IFA$=""THEN345
  printSemi(F("A-bort, C-ont, E-dit, L-ist, or S-ave Message? "));
  uc = 1;
  //gosub1005();
  if (gosub1005()==255) goto line20;
  aStr[1] = '\0';
  if (aStr[0]=='\0') goto line345;

  //350 LN=INSTR("ACELS",A$):IFLN=0THEN345ELSEONLN GOTO385,355,360,375,380
  ln = instr("ACELS", aStr);
  if (ln==0) goto line345;
  if (ln==1) goto line385;
  if (ln==2) goto line355;
  if (ln==3) goto line360;
  if (ln==4) goto line375;
  if (ln==5) goto line380;

line355:
  //355 IFA<10THENPRINT"Continue your message:":GOTO335ELSE340
  if (a<MAX_LINE)
  {
    printSemi(F("Continue your message:"));
    goto line335;
  } 
  else goto line340;

line360:  
  //360 PRINT"Edit line 1 -"A":";:GOSUB1005:LN=VAL(LEFT$(A$,2)):IFLN<1ORLN>A THEN345
  printSemi(F("Edit line 1 -"));
  printSemi(a);
  printSemi(F(":"));
  //gosub1005();
  if (gosub1005()==255) goto line20;
  aStr[2] = '\0';
  ln = atoi(aStr);
  if (ln<1 || ln>a) goto line345;

  //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();
  if (gosub1005()==255) goto line20;
  aStr[INPUT_SIZE] = '\0'; // 1.4
  if (aStr[0]=='\0')
  {
    print(F("*Unchanged*"));
  }
  else
  {
    strncpy(msArray[ms][ln], aStr, INPUT_SIZE);
    print(F("*Corrected*"));
  }

  //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;

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)):IFA<1ORA>MS THEN115
  printSemi(F("Read Message 1 -"));
  printSemi(ms);
  printSemi(F(":"));
  //gosub1005();
  if (gosub1005()==255) goto line20;
  aStr[2] = '\0';
  a = atoi(aStr);
  if (a<1 || a>ms) 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<10THEN435
  b = b + 1;
  print(msArray[a-1][b]);
  if (msArray[a-1][b][0]=='\0')
  {
    goto line440;
  }
  else if (b<MAX_LINE) goto line435;

line440:
  //440 PRINT"*End of Message*":GOTO415
  print(F("*End of Message*"));
  goto line415;

  //450 'Scan Titles
line455:
  //455 IFMS=0THENPRINT"The message base is empty.":GOTO115
  if (ms==0)
  {
    print(F("The message base is empty."));
    goto line115;
  }

  //460 CLS:PRINTCL$"Message Titles:":A=0
  cls();
  print((__FlashStringHelper*)clStr);
  print(F("Message Titles:"));
  a = 0;

line465:
  //465 A$=MS$(A,0):PRINTUSING"[##] SB: ";A+1;:B=INSTR(A$,"\"):C=INSTR(B+1,A$,"\"):PRINTRIGHT$(A$,LEN(A$)-C):PRINTTAB(5)"TO: "LEFT$(A$,B-1):A=A+1:IFA<MS THEN465
  strncpy(aStr, msArray[a][0], INPUT_SIZE);
  printUsingSemi("[##] SB: ", a+1);
  b = instr(aStr, "\\");
  c = instr(b+1, aStr, "\\");
  print(right(aStr, strlen(aStr)-c));
  printTab(5);
  printSemi(F("TO: "));
  {
    char tempStr[TO_SIZE];
    strncpy(tempStr, aStr, b-1);
    tempStr[b-1] = '\0';
    print(tempStr);
  }
  a = a + 1;
  if (a<ms) goto line465;

  //470 PRINT"*End of Messages*":GOTO115
  print(F("*End of Messages*"));
  goto line115;

  //500 '%SYSOP MENU%
line505:
  //505 IFLV<9THENA$="Z":GOTO130
  if (lv<9)
  {
    strncpy_P(aStr, PSTR("Z"), INPUT_SIZE);
    goto line130;
  }

  //510 PRINT"PASSWORD?";:GOSUB1005:IFA$<>"?DROWSSAP"THENPRINT"Thank You!":GOTO115
  printSemi(F("PASSWORD?"));
  //gosub1005();
  if (gosub1005()==255) goto line20;
  if (strcmp(aStr, "?DROWSSAP")!=0)
  {
    print(F("Thank You!"));
    goto line115;
  }

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

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

/*---------------------------------------------------------------------------*/
// 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
byte gosub1005()
{
  byte ch; // Used only here, so we can make it local.
  byte count;

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

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

//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);
}

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

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

// 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; i<length; i++) printCharSemi(charcode);
}

// RIGHT$(str, length)
// Note: Modifies the passed in string, which is okay for our purpose but
// would not be suitable as a generic replacement for RIGHT$.
char *right(char *str, byte length)
{
  return &str[strlen(str)-length];
}

// INSTR(first, str, substr)
// Starting at pos, return position of substr in str, or 0 if not found.
int instr(byte pos, char *str, char *substr)
{
  if (pos<1) return 0;
  return instr(aStr+pos, substr) + pos;
}
int instr(char *str, char *substr)
{
  char *ptr;

  ptr = strstr(str, substr);
  if (ptr==NULL) return 0; // No match?
  if (ptr==&str[strlen(str)]) return 0; // Matched the \0 at end of line?
  return ptr-str+1;
}

/*---------------------------------------------------------------------------*/
// Input functions.

#ifdef ENET_PORT
byte lineinput(char *cmdLine, byte len)
{
  return telnetInput(client, cmdLine, len);
}
#else
// LINE INPUT str
// Read string up to len bytes. This code comes from my Hayes AT Command
// parser, so the variables are named differently.
#define CR           13
#define BEL          7
#define BS           8
#define CAN          24
byte lineinput(char *cmdLine, byte len)
{
  int     ch;
  byte    cmdLen = 0;
  boolean done;

  done = false;
  while(!done)
  {
    //ledBlink();

    ch = -1; // -1 is no data available

    if (Serial.available()>0)
    {
      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 CAN:
       print(F("[CAN]"));
       cmdLen = 0;
       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 (cmdLen<len)
      {
        if ((ch>31) && (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;
}
#endif

// 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...

//SD Card routines
#if defined(SD_PIN)
#define TEMP_SIZE 4
#define FNAME_MAX (8+1+3+1)
boolean initSD()
{
  static bool sdInit = false;

  if (sdInit==true) return true;

  printSemi(F("Initializing SD card..."));
  pinMode(SD_PIN, OUTPUT);
  if (!SD.begin(SD_PIN))
  {
    print(F("initialization failed."));
    return false;
  }
  print(F("initialization done."));
  sdInit = true;

  return true;
}
#endif

//560 OPEN"I",#-1,"USERLOG":INPUT#-1,CL,NM:FORA=0TONM:INPUT#-1,NM$(A):NEXTA:CLOSE
void loadUserlog()
{
#if defined(SD_PIN)
  File myFile;
  char tempStr[TEMP_SIZE];
  char filename[FNAME_MAX];

  if (!initSD()) return;

  strncpy_P(filename, PSTR("USERLOG"), FNAME_MAX);

  myFile = SD.open(filename, FILE_READ);

  if (myFile)
  {
    printSemi(filename);
    print(F(" opened."));
    fileReadln(myFile, tempStr, TEMP_SIZE);
    cl = atoi(tempStr);
    Serial.print(F("cl = "));
    Serial.println(cl);
    fileReadln(myFile, tempStr, TEMP_SIZE);
    nm = atoi(tempStr);
    Serial.print(F("nm = "));
    Serial.println(nm);
    for (a=0; a<=nm; a++)
    {
      fileReadln(myFile, nmArray[a], ULOG_SIZE);
      Serial.print(a);
      Serial.print(F(". "));
      Serial.println(nmArray[a]);
    }  
    myFile.close();
  }
  else
  {
    printSemi(F("Error opening "));
    print(filename);
  }
#else  
  print(F("(USERLOG would be loaded from tape here.)"));
#endif
}

//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()
{
#if defined(SD_PIN)
  File myFile;
  char tempStr[TEMP_SIZE];
  char filename[FNAME_MAX];

  if (!initSD()) return;

  strncpy_P(filename, PSTR("MSGBASE"), FNAME_MAX);

  myFile = SD.open(filename, FILE_READ);
  if (myFile)
  {
    printSemi(filename);
    print(F(" opened."));
    fileReadln(myFile, tempStr, TEMP_SIZE);
    ms = atoi(tempStr);
    Serial.print("ms = ");
    Serial.println(ms);
    for (a=0; a<=ms-1; a++)
    {
      for (b=0; b<=MAX_LINE; b++)
      {
        fileReadln(myFile, msArray[a][b], INPUT_SIZE);
        Serial.print(F("msArray["));
        Serial.print(a);
        Serial.print(F("]["));
        Serial.print(b);
        Serial.print(F("] = "));
        Serial.println(msArray[a][b]);
      }
    }  
    myFile.close();
  }
  else
  {
    printSemi(F("Error opening "));
    print(filename);
  }
#else
  print(F("(MSGBASE would be loaded from tape here.)"));
#endif
}

#if defined(SD_PIN)
//byte fileReadln(File myFile, char *buffer, byte count)
{
  char ch;
  int  pos;

  pos = 0;
  while(myFile.available() && pos<count)
  {
    ch = myFile.read();
    if (ch==CR)
    {
      buffer[pos] = '\0';
      break;
    }
    if (ch>=32)
    {
      //Serial.print(ch);
      buffer[pos] = ch;
      pos++;
    }
  }
  if (pos>=count) buffer[pos] = '\0';
  //Serial.println();
  return pos;
}
#endif

//610 OPEN"O",#-1,"USERLOG":PRINT#-1,CL,NM:FORA=0TONM:PRINT#-1,NM$(A):NEXTA:CLOSE
void saveUserlog()
{
#if defined(SD_PIN)
  File myFile;
  char filename[FNAME_MAX];

  if (!initSD()) return;

  strncpy_P(filename, PSTR("USERLOG"), FNAME_MAX);

  if (SD.exists(filename)==true) SD.remove(filename);

  myFile = SD.open(filename, FILE_WRITE);
  if (myFile)
  {
    printSemi(filename);
    print(F(" created."));
    myFile.println(cl);
    Serial.print(F("cl = "));
    Serial.println(cl);
    myFile.println(nm);
    Serial.print(F("nm = "));
    Serial.println(nm);
    for (a=0; a<=nm; a++)
    {
      myFile.println(nmArray[a]);
      Serial.print(a);
      Serial.print(F(". "));
      Serial.println(nmArray[a]);
    }
    myFile.close();
  }
  else
  {
    print(F("Error creating file."));
  }
#else
  print(F("save USERLOG"));
#endif
}

//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()
{
#if defined(SD_PIN)
  File myFile;
  char filename[FNAME_MAX];

  if (!initSD()) return;

  strncpy_P(filename, PSTR("MSGBASE"), FNAME_MAX);

  if (SD.exists(filename)==true) SD.remove(filename);

  myFile = SD.open(filename, FILE_WRITE);
  if (myFile)
  {
    printSemi(filename);
    print(F(" created."));
    myFile.println(ms);
    for (a=0; a<=ms-1; a++)
    {
      for (b=0; b<=MAX_LINE; b++)
      {
        myFile.println(msArray[a][b]);
        Serial.print(F("msArray["));
        Serial.print(a);
        Serial.print(F("]["));
        Serial.print(b);
        Serial.print(F("] = "));
        Serial.println(msArray[a][b]);
      }
    }
    myFile.close();
  }
  else
  {
    print(F("Error creating file."));
  }
#else
  print(F("save MSGBASE"));
#endif
}

/*---------------------------------------------------------------------------*/
// 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);
void printTab(byte column)
{
  while(tabPos<column)
  {
    printSemi(F(" ")); // Print, and increment tab position.
  }
}

// PRINT,
// NOTE: DECB doesn't add a carraige return after a comma.
void printComma()
{
  printTab(tabPos + (16-(tabPos % 16)));
}

// PRINT "STRING";
// For normal strings in RAM.
void printSemi(const char *string)
{
  tabPos = tabPos + Serial.print(string);
#if defined(ENET_PORT)
  client.print(string);
#endif
}
// For strings in Flash.
void printSemi(const __FlashStringHelper *string)
{
  tabPos = tabPos + Serial.print(string);
#if defined(ENET_PORT)
  client.print(string);
#endif
}
// For printing a byte as a number (0-255).
void printSemi(byte num)
{
  Serial.print(F(" "));
  tabPos = tabPos + Serial.print(num);
  Serial.print(F(" "));
  tabPos = tabPos + 2;
#if defined(ENET_PORT)
  client.print(F(" "));
  client.print(num);
  client.print(F(" "));
#endif
}
// For printing a single character.
void printCharSemi(char ch)
{
  tabPos = tabPos + Serial.print(ch);
#if defined(ENET_PORT)
  client.print(ch);
#endif
}
// For printing a byte as a number with no spaces.
void printNumSemi(byte num)
{
  tabPos = tabPos + Serial.print(num);
#if defined(ENET_PORT)
  client.print(num);
#endif
}

// PRINT
void print(void)
{
  Serial.println();
#if defined(ENET_PORT)
  client.println();
#endif
}

// PRINT "STRING"
void print(const char *string)
{
  Serial.println(string);
  tabPos = 0;
#if defined(ENET_PORT)
  client.println(string);
#endif
}
void print(const __FlashStringHelper *string)
{
  Serial.println(string);
  tabPos = 0;
#if defined(ENET_PORT)
  client.println(string);
#endif
}

// PRINT I
void print(int num)
{
  Serial.print(F(" "));
  Serial.println(num);
  tabPos = 0;
#if defined(ENET_PORT)
  client.print(F(" "));
  client.println(num);
#endif
}

// PRINT USING(format, number);
// NOTE: This only emulates printing positive integers, which is the
// only way it is used by this program.
void printUsingSemi(char *format, byte num)
{
  byte i;
  byte fmtDigits;
  byte numDigits;
  byte tempNum;

  i = 0;
  while(format[i]!='\0')
  {
    if (format[i]!='#') {
      printCharSemi(format[i]);
      i++;
      continue;
    }
    else
    {
      // Start counting the run of #s.
      // Find end of #'s to know how many to use.
      fmtDigits = 0;
      while(format[i]=='#' && format[i]!='\0')
      {
        fmtDigits++;
        i++;
      }
      // Now we know how many # (digits).
      tempNum = num;
      numDigits = 1;
      while(tempNum>10)
      {
        tempNum = tempNum/10;
        numDigits++;
      }
      while(numDigits<fmtDigits)
      {
        printSemi(F(" "));
        fmtDigits--;
      }
      printNumSemi(num);
    }
  }
}

/*---------------------------------------------------------------------------*/
// Show some stuff functions.

// Emit some configuration information.
void showConfig()
{
  print((__FlashStringHelper*)brStr);
  print(F("*ALL RAM* Configuration:"));
  printSemi(F("Userlog size :"));
  print(MAX_USERS+1);
  printSemi(F("Input size   :"));
  print(INPUT_SIZE);
  printSemi(F("Username size:"));
  print(NAME_SIZE);
  printSemi(F("Password size:"));
  print(PSWD_SIZE);
  printSemi(F("Msg base size:"));
  print(MAX_MSGS+1);
  printSemi(F("Message lines:"));
  print(MAX_LINE+1);
#if defined(SD_PIN)
  print(F("SD card      : Enabled"));
#endif
#if defined(ENET_PORT)
  print(F("Ethernet     : Enabled"));
#endif
  printSemi(F("ESTIMATED MEM:"));
  printSemi(ULOG_MEM + MBASE_MEM);
  print(F("bytes."));
  printSemi(F("Free RAM     :"));
  print(freeRam());
  print((__FlashStringHelper*)brStr);
}

void showLoginMessage()
{
  print(F("\nYou are connected to an Arduino UNO R3, a small computer thing you can buy\n"
    "from Radio Shack for $29.99 (or around $22 online). It has 2K of RAM and\n"
    "32K of Flash for program storage. It has a SainSmart Ethernet Shield ($17.99)\n"
    "attached to it. The software running here is a line-by-line port of my\n"
    "*ALL RAM* BBS program written in 1983. The original Extended Color BASIC\n"
    "code was converted as literally as possible to Arduino C. It's a travesty.\n"
    "\n"
    "This BBS program was designed to run on a cassette based TRS-80 Color\n"
    "Computer with 32K of RAM. All messages and users were stored in memory.\n"
    "Obviously, with only 2K of RAM, this is not possible, so this version has\n"
    "been configured to allow only a few users and a teensy tiny message base\n"
    "(smaller than Twitter posts!). Enjoy the experiment!\n"
    "\n"
    "(If userlog is full, use the Sysop password 'TEST'.)"));
}

void showCoCoHeader()
{
  cls();
  print(F("EXTENDED COLOR BASIC 1.1\n"
    "COPYRIGHT (C) 1982 BY TANDY\n"
    "UNDER LICENSE FROM MICROSOFT\n"
    "\n"
    "OK\n"
    "CLOAD\"ALLRAM\"\n"
    "RUN"));
}

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

// Debug and utility functions.
//

unsigned int freeRam() {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

void showFreeRam()
{
  printSemi(F("Free RAM:"));
  print(freeRam());
}

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

void showUserlog()
{
  int i;
  printSemi(F("Users: "));
  print(nm);
  for (i=0; i<=nm; i++) {
    printSemi(i);
    printSemi(F(". "));
    print(nmArray[i]);
  }
}

void showMessageBase()
{
  int i,j;

  for (int i=0; i<ms; i++)
  {
    for (int j=0; j<=MAX_LINE; j++)
    {
      printSemi(F("msArray["));
      printSemi(i);
      printSemi(F(","));
      printSemi(j);
      printSemi(F("] = "));
      print(msArray[i][j]);
    }
  }
}


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

//0 REM *ALL RAM* BBS Editor 1.0
//1 REM   Shareware / (C) 1983
//2 REM     By Allen Huffman
//3 REM  110 Champions Dr, #811
//4 REM     Lufkin, TX 75901
//5 FORA=0TO8:READA$:POKE1024+A,VAL("&H"+A$):NEXTA:EXEC1024:DATAC6,1,96,BC,1F,2,7E,96,A3
//10 CLEAR21000:DIMNM$(200),MS$(19,10),A$,F$,S$,T$,NM$,PW$,A,B,C,LN,LV,MS,NM,PR:PR=80
//15 CLS:PRINTTAB(3)"*ALL RAM* EDITOR COMMANDS:":printING$(32,45)
//20 PRINTTAB(4)"1. CREATE USERLOG",TAB(4)"2. LOAD USERLOG/MSG BASE",TAB(4)"3. SAVE USERLOG/MSG BASE",TAB(4)"4. PRINT USERLOG",TAB(4)"5. PRINT MESSAGES",TAB(4)"6. EDIT USERS",TAB(4)"7. KILL MESSAGES",TAB(4)"8. QUIT"
//25 PRINT@392,"ENTER FUNCTION :"
//30 A$=INKEY$:IFA$=""THEN30ELSEPRINT@408,A$:LN=VAL(A$):IFLN<1ORLN>8THENSOUND50,1:GOTO25
//35 SOUND200,1:ONLN GOTO55,105,155,205,255,305,405,40
//40 STOP
//50 'Create Userlog
//55 CLS:PRINTTAB(7)"SYSOP INFORMATION:":printING$(32,45)
//60 PRINT@128,"SYSOP'S NAME:    (20 CHARACTERS)>";:LINEINPUTA$:IFA$=""ORLEN(A$)>20THENSOUND50,1:GOTO15ELSENM$=A$
//65 PRINT@192,"PASSWORD    :     (8 CHARACTERS)>";:LINEINPUTA$:IFA$=""ORLEN(A$)>8THENSOUND50,1:GOTO15ELSEPW$=A$
//70 PRINT@297,"*VERIFY ENTRY*":PRINT:PRINT"NAME :"NM$:PRINT"PSWD :"PW$:PRINT@456,"IS THIS CORRECT?";
//75 LINEINPUTA$:IFLEFT$(A$,1)<>"Y"THENSOUND50,1:GOTO55
//80 NM$(0)=NM$+"\"+PW$+"9":GOTO15
//100 'Load Userlog/Msg Base
//105 CLS:PRINTTAB(5)"LOAD USERLOG/MSG BASE:":printING$(32,45)
//110 LINEINPUT" READY TAPE, THEN PRESS ENTER:";A$:PRINT@168,"...ONE MOMENT..."
//115 OPEN"I",#-1,"USERLOG":PRINT@232,"LOADING  USERLOG":INPUT#-1,CL,NM:FORA=0TONM:INPUT#-1,NM$(A):NEXTA:CLOSE
//120 OPEN"I",#-1,"MSG BASE":PRINT@240,"MSG BASE":INPUT#-1,MS:FORA=0TOMS-1:FORB=0TO10:INPUT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:GOTO15
//150 'Save Userlog/Msg Base
//155 CLS:PRINTTAB(5)"SAVE USERLOG/MSG BASE:":printING$(32,45)
//160 LINEINPUT" READY TAPE, THEN PRESS ENTER:";A$:PRINT@168,"...ONE MOMENT...":MOTORON:FORA=1TO1000:NEXTA
//165 PRINT@232,"SAVING   USERLOG":OPEN"O",#-1,"USERLOG":PRINT#-1,CL,NM:FORA=0TONM:PRINT#-1,NM$(A):NEXTA:CLOSE
//170 PRINT@240,"MSG BASE":OPEN"O",#-1,"MSG BASE":PRINT#-1,MS:FORA=0TOMS-1:FORB=0TO10:PRINT#-1,MS$(A,B):NEXTB:NEXTA:CLOSE:GOTO15
//200 'Print Userlog
//205 IFNM$(0)=""THENPRINT@454,"*USERLOG NOT LOADED*":SOUND50,1:GOTO25
//210 CLS:PRINTTAB(9)"PRINT USERLOG:":printING$(32,45)
//215 LINEINPUT"    PRESS ENTER WHEN READY:";A$:PRINT@169,"...PRINTING..."
//220 PRINT#-2,TAB((PR-30)/2)"[*ALL RAM* BBS System Userlog]":PRINT#-2,"":PRINT#-2,TAB((PR-46)/2)"[###]  [        NAME        ]  [PASSWORD]  [L]"
//225 FORA=0TONM: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))
//230 A$="000........................................0":B=LEN(STR$(A))-1:MID$(A$,4-B,B)=RIGHT$(STR$(A),B):MID$(A$,8,LEN(NM$))=NM$:MID$(A$,32,LEN(PW$))=PW$:MID$(A$,44,1)=RIGHT$(STR$(LV),1)
//235 PRINT@238,A:PRINT#-2,TAB((PR-44)/2)A$:NEXTA:GOTO15
//250 'Print Messages
//255 IFMS$(0,0)=""THENPRINT@454,"*MSG BASE NOT LOADED*":SOUND50,1:GOTO25
//260 CLS:PRINTTAB(8)"PRINT  MESSAGES:":printING$(32,45)
//265 LINEINPUT"    PRESS ENTER WHEN READY:";A$:PRINT@169,"...PRINTING..."
//270 PRINT#-2,TAB((PR-30)/2)"[*ALL RAM* BBS System Messages]":PRINT#-2,""
//275 FORA=0TOMS-1:A$=MS$(A,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)
//280 PRINT@238,A+1:B=(PR-64)/2:PRINT#-2,TAB(B)"Message #"A:PRINT#-2,TAB(B)"TO :"T$:PRINT#-2,TAB(B)"FR :"F$:PRINT#-2,TAB(B)"SB :"S$:PRINT#-2,STRING$(64,45):C=0
//285 C=C+1:PRINT#-2,TAB(B)MS$(A,C):IFMS$(A,C)=""THEN290ELSEIFC<10THEN285
//290 PRINT#-2,"":NEXTA:GOTO15
//300 'Edit Users
//305 IFNM$(0)=""THENPRINT@454,"*USERLOG NOT LOADED*":SOUND50,1:GOTO25
//310 CLS:PRINTTAB(10)"EDIT  USERS:":printING$(32,45):A=0
//315 PRINT@70,"USERS ON SYSTEM:"NM
//320 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))
//325 PRINT@128,"USER #"A:PRINT:PRINT"NAME: "NM$:PRINT"PSWD: "PW$:PRINT"LVL :"LV
//330 PRINT@320,STRING$(32,45)TAB(4)"D-LET   UP-BACK   J-UMP",TAB(4)"E-DIT   DN-NEXT   M-ENU"
//335 PRINT@456,"ENTER FUNCTION :"
//340 A$=INKEY$:IFA$=""THEN340ELSEPRINT@472,A$;:LN=INSTR("DEJM"+CHR$(94)+CHR$(10),A$):IFLN=0THENSOUND50,1:GOTO335
//345 SOUND200,1:ONLN GOTO350,365,385,15,390,395
//350 IFA=0THENSOUND1,5:GOTO335
//355 IFA=NM THENNM=NM-1:A=A-1:GOTO315
//360 FORB=A TONM:NM$(B)=NM$(B+1):NEXTB:NM=NM-1:GOTO315
//365 PRINT@198,;:LINEINPUTA$:IFA$=""ORLEN(A$)>20THENPRINT@198,NM$ELSENM$=A$
//370 PRINT@230,;:LINEINPUTA$:IFA$=""ORLEN(A$)>8THENPRINT@230,PW$ELSEPW$=A$
//375 PRINT@262,;:LINEINPUTA$:B=VAL(A$):IFB<1ORB>9THENPRINT@261,LV ELSELV=B
//380 NM$(A)=NM$+"\"+PW$+RIGHT$(STR$(LV),1):GOTO335
//385 PRINT@456," JUMP TO USER # ";:LINEINPUTA$:B=VAL(A$):IFB<0ORB>NM THENSOUND1,5:GOTO335ELSEA=B:GOTO320
//390 A=A-1:IFA<0THENA=NM
//391 GOTO315
//395 A=A+1:IFA>NM THENA=0
//396 GOTO315
//400 'Kill Messages
//405 IFMS$(0,0)=""THENPRINT@454,"*MSG BASE NOT LOADED*":SOUND50,1:GOTO25
//410 CLS:PRINTTAB(9)"KILL MESSAGES:":ING$(32,45)
//415 PRINT@96,"DELETE MESSAGE # 1 -"MS":";:LINEINPUTA$:A=VAL(A$):IFA<1ORA>MS THEN15
//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)
//425 PRINT:PRINT"TO: "T$:PRINT"FR: "F$:PRINT"SB: "S$
//430 PRINT:LINEINPUT"DELETE THIS?";A$:IFLEFT$(A$,1)<>"Y"THEN410
//435 IFA=MS THENMS=MS-1:GOTO410
//440 FORB=A-1 TOMS-2:FORC=0TO10:MS$(B,C)=MS$(B+1,C):NEXTC:NEXTB:MS=MS-1:GOTO410

// End of file.

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

*** 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].

//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[c] = ch - 32;
    // 1015 IFCH=92THENMID$(A$,C,1)="/"
    if (ch == 92)
        aStr[c] = '/';
    // 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] = '&#92;&#48;';
  done = true;
  break;

case BS:
  if (cmdLen&gt;0)
  {
    printCharSemi(BS);
    printSemi(F(&quot; &quot;));
    printCharSemi(BS);
    cmdLen--;
  }
  break;

default:
  // If there is room, store any printable characters in the cmdline.
  if (cmdLen31) &amp;&amp; (ch&lt;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

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…

Extended Color BASIC to Arduino Sketch, part 3

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

Just because something can be done, doesn’t mean it should.

“Why are you converting thirty year old BASIC programs to C?” you might ask… Well, mostly just to see how easily it could be done, but I admit, nostalgia has a big part in this. I am not quite sure what led me to looking in to my 1983 BBS program, but once I saw it running on the XRoar CoCo emulator, it brought back all kinds of fond memories of sitting in front of a TV set typing in lines of BASIC code.

I believe I got in to computers at the very best time. A few years earlier, and it was punch cards and flip switches. A decade or so later, and computers were becoming appliances. There was a bit of time in between when owning a computer primarily meant programming a computer. I got in to it late enough that I didn’t have to be a hardware guy to build my own computer, and early enough that I was still compelled to figure out what to do at the blinking cursor after the “OK” prompt. There just wasn’t nearly as much off-the-shelf software to buy back then, you see. Especially on my allowance.

I realized tonight that there were many like myself who once wrote their own programs and experimented. As time moved on, most of us ended up with a PC or Mac or even Linux machine, and do little more than run software written by others. Thirty years ago, if we wanted to print up a sheet of return address mailing labels, we would type in a few lines of BASIC and be done.

10 FOR I=1 TO 25
20 PRINT #-2, "Allen C. Huffman"
30 PRINT #-2, "PO Box 22031"
40 PRINT #-2, "Clive IA 50325-94014"
50 PRINT #-2
60 NEXT I

Today, if we don’t have a template for MS-Word, we might end up Googling to find some freeware or shareware offering. (I suppose we have also passed up the pre-Internet times when folks had to go to a computer store like Babbages or CompUSA to look for something on the shelf to buy.)

But what about all those BASIC coders that perhaps haven’t ever written even the simplest line of code for a Windows PC? Did they all just decide they no longer liked it? Or did the environment change enough that programming just wasn’t as accessible?

32K CoCo BASIC memory.

Certainly, modern computers no longer power up instantly and present you with a place to type and run a program…

My recent exposure to the Arduino has made me want to program for fun again. Just like my TRS-80 Color Computer, I purchased my Arduino UNO R3 at a nearby Radio Shack. But unlike my CoCo, the Arduino required a host PC/Mac/Linux machine to make it do anything, and then you had to know C/C++. Fortunately, I do… I learned it on my CoCo under OS-9 back in the late 1980s! But what about all those non-OS-9 CoCo owners who stuck with Disk Extended Color BASIC? For them, an Arduino would present quite a learning curve.

But could someone who knows BASIC learn enough to write (very poor) C and actually use an Arduino? I think they could.

void program()
{
    for (i = 0; i < = 25; i++)
    {
        Serial.println("Allen C. Huffman");
        Serial.println("P.O. Box 22031");
        Serial.println("Clive IA 50325-9401");
        Serial.println();
    }
}

Tonight, my *ALL RAM* BBS system is fully functional on my Arduino. The code I originally wrote thirty years ago in BASIC was well structured, with subroutines for items like input and output which translated easily to C functions. Program logic was a mess of if statements and gotos, but since C supports both, it was fairly easy to make the jump. But, it wasn’t pretty. Instead of line numbers, I created C lables in the format of “lineXXX:” where XXX was the desired line number:

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)
{
    Serial.println("Sorry, your time on-line is up.");
    goto line210;
}
else if (ky > 180)
{
    Serial.println("Please complete your call soon.");
}

And, to make things flow a bit better, I created C functions to replicate some of the BASIC keywords:

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

Some things did not have a direct equivalent in C, but could be done in a simple, brute-force manner:

//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;

I was surprised at how well one could program C as if it were BASIC.

As of tonight, *ALL RAM* BBS runs on my Arduino, complete with userlog and message base. Due to limited memory, there is only room for a few users and a few TINY (Twitter-sized) messages, but it all works. I had to do some Arduino tricks to get strings and such moved in to Flash to free up precious RAM, and once I did that, I even had room to use some SD memory card routines for replacing the original cassette tape load/save routines. (Yes, I realize this is rather silly. There’s no reason to run a RAM-based BBS if you have access to disk storage, but I never said this was a practical experiment.)

I even managed to use my Arduino Ethernet shield and telnet in to the micro BBS. The quick modifications I made to allow this weren’t fully baked — once the connection was broken I had to reset the Arduino — but I believe I will work on that next. (Though probably not with SD card support. I just don’t think there’s enough RAM for both.)

So, in coming days or weeks, I will try to find time to share the code, or at least examples of how I created BASIC-like functions in C.

Until then…

*ALL RAM* BBS Logoff Screen

P.S. – For the younger ones of you out there, yes, there was a time when lowercase was not available. The original TRS-80 Color Computer presented inverse letters to represent lowercase. It wasn’t until the later model “Tandy” Color Computer 2s that the machine got true lowercase. (My CoCo 1 had an aftermarket lowercase board installed in it…) I feel so old.

Extended Color BASIC to Arduino Sketch, part 2

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

Yesterday, I shared a silly little project I was undertaking where I was going to port a BASIC program to the Arduino, line-by-line. I am glad to say I have successfully done just that, but the practicality of such accomplishment is very questionable.

For programs that were not created to use 21K of RAM for a memory-resident userlog and message base (like my *ALL RAM* BBS program does), it seems fairly easy to port over Microsoft BASIC code and get it running on the Arduino with surprisingly easy changes. I am tempted to find some classic BASIC programs (like ELIZA or Hunt the Wumpus) and try to convert them, as well, just to see if 2K of RAM is enough for hopefully less memory-hungry code.

So let’s talk a bit about memory. The Radio Shack TRS-80 Color Computer came out in 1980 with a 4K model (later generations would support as much as 512K, or megabytes through third party suppliers). In a 4K system, all of the BASIC code took memory, as well as any variables the program used. I expect working in an Arduino is similar to the 1980 experience, except for more program space.

On a 4K computer, if you had a BASIC program that took up 3K, you only had 1K left for variables. On the Arduino UNO, there is 2K of RAM, and 32K of Flash storage to hold the program. So, that same 3K program that only had 1K of RAM for variables on a 4K CoCo would do much better on an Arduino since it could store the 3K program in Flash, then have the full 2K for variables.

32K CoCo BASIC memory.

Unfortunately, my BBS program was designed to work on a 32K CoCo. These early 8-bit computers, even with 64K upgrades, still had a BASIC that only recognized 32K (without loading some extra patch program). And, on power up, some of that memory was reserved to hold four graphics pages. Thus, a 32K CoCo would show 24874 bytes available in BASIC.

You could adjust the amount of memory reserved for graphics screens higher or lower with the “PCLEAR” command. A “PCLEAR 8” reduced memory down to 18727 bytes. 6K (6144 bytes, or 1.5K per graphics page) was consumed. For programs not making use of these graphics, we wanted to get rid of that reserve and use it ourselves. Unfortunately, the PCLEAR command only allowed going down to “PCLEAR 1”, which gave 29479 bytes free for BASIC. That’s a nice bit of extra, but someone clever figured out how to achieve a PCLEAR 0 and get the most memory we could use without patching BASIC: 31015 bytes free.

Note that, once you plugged in a Radio Shack Disk Controller and added Disk Extended Color BASIC, the memory available went down a bit due to overhead of supporting the disk system. For this article, we are looking strictly at a circa 1980 style CoCo with a cassette recorder only.

And what does this have to do with the Arduino? I wanted to make sure you understood the limits I was facing when trying to port a cassette based BBS program to a machine with only 2K of RAM (and not all of that is usable by the user program).

My end result was a fully functional clone of the *ALL RAM* BBS, but with some rather impractical limits. For my demo system, I only allocated enough space to support about four users. Even I have more friends than that. And for messages, the limit was about four messages, each having just two or three lines. I would make a comment about how useless this would be, but Twitter seems to have proven people will use something that even gives less space than my 2K BBS.

So how did I do it? One word: gotos. Lots and lots of gotos. My translation was literal. Even though there are much better, more efficient ways to write this program (which I plan to undertake), I thought it would be fun to see just how easily BASIC could be coded in C.

I started out by taking an ASCII listing of the BASIC program, and adding “//” comment markers to the start of each line. I pasted that in to a fresh Arduino sketch, so it looked like this:

//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
//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
//15 CL$=CHR$(12)+CHR$(14):BR$="*==============*==============*":GOSUB555
//20 CLS:PRINTTAB(6)"*ALL RAM* BBS SYSTEM":PRINT"USERS:"NM,"CALLS:"CL:PRINTTAB(5)"SYSTEM AWAITING //25 A$="Welcome To *ALL RAM* BBS!":GOSUB1055:KY=0:CL=CL+1
//30 PRINT:PRINT"Password or 'NEW' :";:UC=1:GOSUB1005:PS$=A$:IFA$=""ORA$="NEW"THEN55ELSEPRINT"Checking: ";:A=0

And thus it began. I would go line-by-line and try to write C code for each statement. All C variables would be made global, just like they were in BASIC, and blocks of code called by GOSUBs would become standard C functions (since they return just like BASIC). But, for GOTOs, I decided to just use the C goto, and added labels in the format of “line125:” so I could later “goto line125;” in C.

It looked like it just might work… (And, since I started this story in reverse, you already know that it did, but I will share some of the ways I did it in a future posting.)

Until then… Enjoy having more than 2K of RAM!

Extended Color BASIC to Arduino Sketch

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

  • 2014/03/16 Update: The source code to this is now on GitHub. Check the Arduino link at the top of each page of this site.

In 1983, I released a BBS (bulletin board system) for Tandy/Radio Shack TRS-80 Color Computer (CoCo). Unlike all of the other ones available at the time, mine was rather unique. Instead of requiring “3-4 disk drives” to operate, mine would work with… zero disk drives.

The *ALL RAM* BBS system was designed to run on a 32K CoCo with a cassette tape deck. On startup, it would read in the userlog and message base in to RAM, and then as users called in, messages they posted would be stored in memory. When the SysOp (system operator) needed to use the computer, or was ready to shut things down for the day, he would save the system back to tape.

It was small and primitive, but considering how primitive the state-of-the-art was back then, it was still pretty usable.

The software was meant to be marketed by a CoCo software company, but things ended up not happening, and the software was eventually released as freeware (even though, back then, we did not yet have the names “shareware” or “freeware”. I still remember an experiment a software company did for something they called “pass the hat software”… I guess their name just wasn’t catchy enough).

But I digress.

Tonight, thanks to the XRoar Color Computer emulator, I was able to get my thirty year old software running again thanks to the software being available at the wonderful BBS Documentary website.

My oh my. What a simpler time.

Tonight, I wondered how hard it would be to translate Microsoft Extended Color BASIC to Arduino C. After all, C has a “goto” statement…

And here is part of the *ALL RAM* BBS system running on an Arduino:

The 1983 Radio Shack Color Computer BBS package is back... This time on Arduino!
The 1983 Radio Shack Color Computer BBS package is back… This time on Arduino

The userlog and message base is not yet functional, and likely never will be since the Arduino only has 2K of memory versus 32K on the CoCo 1 it was designed for. But, when I have some time, I will finish it up and maybe support a few users, and a few messages. Perhaps I can even use some of the memory saving tricks I have been using on some work projects to compress things quite a bit.

We shall see.

Hello, *ALL RAM*. Nice to see you again.

P.S. – The *ALL RAM* BBS ran in Houston, Texas under the name Cyclops Castle. The version running there was disk enhanced. Instead of loading and saving to cassette, it used a single disk drive. The software was modified to support dozens of separate message bases, and the user could switch them from the menu. It was quite a feat on a single floppy disk. One day I will have to track down the SysOp, Graham, and see what he remembers from this great experiment.