Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Primitive data transfer script for the Model 100/200

Jerry Stratton, May 27, 2016

As I’ve mentioned a couple of times now, I have recently acquired a TRS-80 Model 100 and a Tandy Model 200, two very early and very impressive laptops.

Since these laptops predated inexpensive memory both in the form of RAM and permanent storage, there isn’t any room on them to store very many BASIC programs. Further, their primary purpose is writing, and it would be nice to get the writing off of the laptops and onto, say, this blog. The laptops both have several communications options, but as they are options from 1983/1984, none of them are Internet, Bluetooth, or WiFi.

I ordered a RS-232 to USB adapter and delved into Python’s serial port extension module, pyserial.

This stuff is ancient; so much about communications nowadays is handled by software that I’d completely forgotten the need for a null modem adapter to switch the transmit and receive lines in RS-232 when connecting two computers directly to each other. Further, it isn’t used enough to justify drivers that don’t crash your computer. After running these tests on my iMac using the Prolific drivers, I now habitually unplug the serial adapter and reboot the computer. If I don’t the computer will reboot itself within a few hours. And this is the most commonly recommended product/driver combination among people who still use RS232.

That working, however, what I needed was very simple. The Model 100/200 does not have any fancy terminal software with downloading functions that automatically recognize filenames and download to the appropriate place. “Download” on these laptops is no more than capturing the incoming data to a file you name, and “upload” is no more than sending out the file you want to upload. I vaguely remember, in the early days of Bulletin Board Services, special codes for noting when a file transfer was about to start so that terminal software would know; these laptops predate that.

And all I really need is to be able to transfer text files back and forth. I chose to make the file transfer location be under my Dropbox folder; I named the folder “TRS80”. This way, I can also put things into it and take things out of it on Editorial or Textastic on the iPad.

Depending on your system, you may need to change the serialPort variable; it appears that on Mac OS X and Linux, the first USB adapter will be called “/dev/tty.usbserial”, but your mileage may vary.

  • port = OldSchoolSerial()
  • print "Use 98N1E for Model 100, 98N1ENN for Model 200."
  • port.timeout = 1
  • port.baudrate = 19200
  • port.bytesize = 8
  • port.parity = serial.PARITY_NONE
  • port.stopbits = serial.STOPBITS_ONE
  • port.xonxoff = True
  • port.rtscts = False
  • port.dsrdtr = False
  • port.port = serialPort
  • port.open()

I subclass serial.Serial as OldSchoolSerial; create an instance of it; and then set those old options that used to bedevil us when connecting to various devices and, if I recall correctly, services. For extra helpfulness, I also print out the correct stat settings for the Model 100 and Model 200 to match these settings.

There is nothing special about these choices. I would have used the default options on the TRS-80s, but the default option is to use the built-in modem.

[toggle code]

  • try:
    • port.loop()
  • except KeyboardInterrupt:
    • print 'Quitting.'
    • port.closePort()

After opening the port, I call the special loop method to wait for commands; on CTRL-C or any other form of keyboard interrupt, it calls the closePort method, to ensure as clean a shutdown as possible using these drivers.

[toggle code]

  • # Close (without flush, it *will* hang)
  • def closePort(self):
    • self.flush()
    • print "Flushed"
    • self.close()
    • print "Closed"

My experience has been that under some circumstances, closing without flushing will always hang the port. The port will be inaccessible until reboot, and the process will be an unkillable zombie until forced shutdown. The computer will never complete a soft shutdown—the unkillable zombie will block it. I mention this mainly because all the examples of pyserial scripting I’ve seen omit the flush. And yes, this made debugging this script annoying.

[toggle code]

  • def loop(self):
    • visitor = 'unknown visitor'
    • while True:
      • command, option = self.readCommand()
      • if command is None:
        • continue
      • elif command == 'HELLO':
        • if option:
          • visitor = 'Model ' + option
        • print 'Greeting', visitor
        • self.writeLine('Hello, ' + visitor + '!')
      • elif command == 'BYE':
        • print visitor, 'leaving.'
        • self.writeLine('Goodbye, ' + visitor + '!')
        • visitor = 'unknown visitor'
      • elif command == 'LIST':
        • print 'Listing directory', dropbox
        • files = [x for x in os.listdir(dropbox) if not x.startswith('.')]
        • if files:
          • self.formattedColumns(files)
        • else:
          • self.writeLine('No files available')
      • elif command in ['UPLOAD', 'REPLACE']:
        • filename = option.strip()
        • if not filename:
          • self.writeLine('Specify filename to upload to.')
        • elif command == 'UPLOAD' and self.fileExists(filename):
          • self.writeLine('File ' + filename + ' exists; consider REPLACE.')
        • else:
          • self.writeLine('Ready to accept ' + filename)
          • self.writeLine('Use ESC or CTRL-C to end transfer.')
          • self.acceptUpload(filename)
      • elif command == 'DOWNLOAD':
        • filename = option.strip()
        • if not filename:
          • self.writeLine('Specify filename to download.')
        • else:
          • self.uploadFile(filename)
      • elif command:
        • print "Unknown command: '" + command + "'"
        • if args.verbose:
          • print len(command)
          • print " ".join(hex(ord(n)) for n in command)
        • self.writeLine('I do not understand ' + command)

If the command is unrecognized, the script will print the command in hex; this was because invisible characters were often what caused the problem, such as backspaces, misplaced line feeds, or carriage returns. Despite the fact that the script reads one character at a time and ignores invalid characters, they can sometimes slip in.

The current commands are:

  • HELLO to introduce yourself somewhat backwardly in SMTP style;
  • BYE to de-introduce yourself;
  • LIST to display the available filenames in the dropbox folder;
  • UPLOAD to transfer a new file from the laptop to the server;
  • REPLACE to transfer a new file from the laptop to the server, potentially erasing any existing file on the server;
  • DOWNLOAD to transfer a file from the server to the laptop.

Commands are of the form COMMAND option where the command is displayed as uppercase and the optional option is displayed in lowercase.

[toggle code]

  • def readCommand(self):
    • self.write(eol)
    • self.write('> ')
    • line = ''
    • inCommand = True
    • foundExtension = False
    • while True:
      • character = self.read(1)
      • if character:
        • if args.verbose:
          • self.display(character)
        • if character == '\r':
          • break
        • elif self.isEscape(character):
          • return None, None
        • elif character == '\b':
          • if line:
            • if line[-1] == ' ':
              • inCommand = True
            • line = line[:-1]
            • self.write('\b \b')
          • continue
        • elif inCommand and character == ' ':
          • inCommand = False
        • elif not inCommand and not foundExtension and character == '.':
          • foundExtension = True
          • pass
        • elif not inCommand:
          • if ord(character) > 64 and ord(character) < 91:
            • #convert uppercase to lowercase if in filename
            • character = chr(ord(character)+32)
          • if ord(character) < 97 or ord(character) > 122:
            • if ord(character) < 48 or ord(character) > 57:
              • continue
        • elif inCommand:
          • if ord(character) > 96 and ord(character) < 123:
            • #convert lowercase to uppercase if in command
            • character = chr(ord(character)-32)
          • if ord(character) < 65 or ord(character) > 90:
            • continue
        • self.write(character)
        • line += character
    • self.write(eol)
    • line = line.strip('\x13\x11 ')
    • if ' ' in line:
      • return line.split(' ')
    • else:
      • return line, ''

On an ESC or CTRL-C, the command is canceled. A carriage return signals the end of the command. And backspacing has to be handled manually, both erasing the character backspaced over from the laptop’s terminal display and removing it from the command string.

Commands must be letters; options can be letters, numbers, and up to one period.

Here’s how a session might go to download a BASIC checkers game and run it:

> HELLO 100
Hello, Model 100!

> LIST
checkers.ba cram.ba cram200.ba
funhouse.ba hello.ba upload.ba
weekday.ba

> DOWNLOAD checkers.ba
<F2>
File to download? check.do
[text of downloaded file shows onscreen]

>
<F2>

The F2 key tells the laptop to start storing incoming data to a file; pressing the F2 key a second time ends the storage.

The BASIC program is now saved on the laptop as “check.do”, with an extraneous cursor marker at the end. To run it, exit to the main screen, enter BASIC, potentially type “new” to clear any existing BASIC program, and “merge "check.do"”.

There will be a wait, and then an error at the end due to the cursor character. You can than run or list the file and, when satisfied that it’s okay, save it and kill the downloaded version:

save "check.ba"

kill "check.do"

Uploading is easier because the command tells the server what filename to save it as. You will have to use ESC or CTRL-C to tell the server that the data has completed uploading.

[toggle code]

  • def uploadFile(self, filename):
    • if self.fileExists(filename):
      • filepath = self.makeUploadPath(filename)
      • print "Sending file", filepath
      • file = open(filepath, 'r')
      • data = file.read()
      • data = data.replace("\n", "\r")
      • self.write(data)
      • file.close()
      • print "Sent file", filepath
    • else:
      • print "No such file."

The download (transfer from server to laptop—upload from the server’s perspective) method attempts to do line-ending conversion. This may end up being a pain, but for the moment it seems to work. I found that without converting linefeeds to carriage returns, BASIC programs would refuse to run; it looked as if the lines were mostly run together.

You can see the rest of the class in the full script (Zip file, 2.6 KB).

Despite the issues with the driver, working with these old workhorses has been a lot of fun. In the future, I intend to use this script on a tiny Linux box dangling from the RS232 port—probably a CHIP. Once I do that, and as I continue to use these laptops, I’ll probably discover ways of making the script more useful.

  1. <- Goodreads advanced search
  2. Poster pixel sizes ->