Not my code, but it is in the public domain. Python routines for talking to a 
tpdd2. 

cheers 

-- 
alex 

> From: "Brian White" <bw.al...@gmail.com>
> To: m...@bitchin100.com
> Sent: Monday, December 2, 2019 7:58:34 AM
> Subject: Re: [M100] Tpdd master routines in C?

> tpddtool is a tppdd client in python
> https://trs80stuff.net/tpdd/
> --
> bkw

> On Sun, Dec 1, 2019, 1:34 PM Stephen Adolph < twospru...@gmail.com > wrote:

>> Ok, thanks Kurt. Yah there may not be an application that has been written 
>> in C
>> on the head end. if the source of PDD.exe was around that might have been one
>> place.
>> I may just translate my ml routines.

>> On Sun, Dec 1, 2019 at 12:51 PM Kurt McCullum < ku...@fastmail.com > wrote:

>>> Got it,

>>> The only code that I have is for the TPDD client tool that I wrote to talk
>>> directly to my TPDD2 drive. It's all in C# if you are interested.

>>> Kurt

>>> On Sun, Dec 1, 2019, at 9:48 AM, Stephen Adolph wrote:

>>>> Hi Kurt, no I mean routines that are the "master" end because TPDD is
>>>> master-slave. IE the M100 end of the link.
>>>> I have my own machine code routines; maybe I have to translate them..

>>>> On Sun, Dec 1, 2019 at 12:40 PM Kurt McCullum < ku...@fastmail.com > wrote:

>>>>> Steve,

>>>>> When you say 'master routines' are you referring to code emulating a TPDD 
>>>>> drive?

>>>>> Kurt

>>>>> On Sun, Dec 1, 2019, at 6:33 AM, Stephen Adolph wrote:

>>>>>> Hi, wondering if there are any TPDD master routines written in C 
>>>>>> available out
>>>>>> there? Maybe someone has something written in a language that could be
>>>>>> translated easily to C?
>>>>>> Thx
>>>>>> Steve
#!/usr/bin/python

# pdd2.py --- A collection of handy python functions for dealing with
# a Tandy Portable Disk Drive 2.

# 2013-10-25
# Bee Nine <hacke...@gmail.com>
# Explicitly released into the Public Domain.

# Note: You'll need to be in either group dialout (check /dev/tty*) or
# run this as root. I suggest the former as the latter is overkill.


# REFERENCES:
# ftp://ftp.whtech.com/club100/ref/comand.tdd
# http://www.club100.org/library/doc/testtdd.html
# http://bitchin100.com/wiki/index.php?title=TPDD_Base_Protocol

# But note, reference documentation is buggy in many places!

# bitchin100 perhaps has the most complete information, but the last
# two sections ("Calculating Checksum" and "Using this Information")
# are just plain wrong. 
# * The checksum is NOT the "number of bytes" but the sum all the bytes. 
# * Space$(24) would give a different checksum than the examples show; so 
#   the characters to pad filenames with are possibly NULLs (chr(0)). 
# * There is a suspicious chr$(13) at the end after the checksum for
#   Status and Format. I notice that both of those sections are written
#   in the first person, perhaps by someone other than the original author?
# * The file suggests that PDD2 units can autobaud, but mine certainly
#   can't. It seems to only talk at 19200. I notice there are solder pads
#   where a switch could be put in. Probably Radio Shack didn't want
#   to waste the money on a feature (slower xfr rate) nobody wanted.

# The "command.tdd" file has some additional information that ought to
# be in the bitchin100 file: 
# * The drive uses RTS/CTS hardware handshaking
# * The filename should be padded to the left with spaces to fill the
#   first six characters. The seventh character must be a period. The
#   next three characters are the optional extension. After that,
#   there should be enough spaces to make the entire string 26
#   characters long.
# * "File" opcode works on files in the directory list, not "blocks". 
# And some possible misinformation:
# * This file mentions that the checksum is actually a sum (good) but
#   does not metion that the result is then complemented (all bits
#   inverted using XOR 255).
# * This file does not mention the opcodes for 'condition' or 'rename'
# * The file has weird stubs for an "unknown" opcode 8, and a mode
#   type for the "Close" and "Delete" opcodes which probably should be
#   removed if they don't actually exist.
# * The "Find" opcode doesn't mention the modes for "previousblock" or "end",
#   and completely forgets the mandatory "F" parameter before the checksum.

# The testtdd.html file is the only reference that mentions the FLOPPY
# test, which is actually a very strange invocation of the STATUS
# opcode (7). Length is 7, data is "^K^LFLOPP", and checksum is "Y".
# (Which is, bizarrely, correct!) I think that FLOPPY is perhaps not a
# special command, but merely a string someone made up so that a valid
# STATUS + checksum could be typed from TELCOM. The results mentioned
# are "q{" for PDD1 and "p|" for PDD2, when no disk is in the drive.
# That fits with the error codes from a normal STATUS return: q (0x71)
# is "disk change error" and p (0x70) is "no disk". Why the errors
# differ for the two drives, who knows?



### Global hash table of opcode names to numbers
# (Add 0x40 to access BANK 1 instead of BANK 0)
optable={'find'      :0x00,
         'open'      :0x01,
         'close'     :0x02,
         'read'      :0x03,
         'write'     :0x04,
         'delete'    :0x05,
         'format'    :0x06,
         'status'    :0x07,
         'condition' :0x0c,
         'rename'    :0x0d,
         }

### Global hash table of mode types to mode number
modetable={
    # For FIND opcode
    'reference'  : 0x00, 
    'first'      : 0x01,
    'next'       : 0x02,
    'previous'   : 0x03,
    'end'        : 0x04,

    # For OPEN opcode
    'write'      : 0x01, 
    'append'     : 0x02,
    'read'       : 0x03
    }

### Global hash table of return opcodes FROM the drive
returntable={
    0x10   :'read',        # [0x0a, len, data, cksum] (if len==0x80, read again)
    0x11   :'find',        # [0x11, 0x1c, filename, size, free, cksum]
    0x12   :'normal',      # [0x0c, 0x01, errorcode, cksum]
    0x15   :'condition',   # [0x0f, 0x01, condition, cksum]

    # This is not documented, but is an error code my PDD2 returned
    # whenever I sent it a 'find' with an invalid length of data.
    # (Filename padded to less than 24 bytes). 
    # It appears to be a variant of the 'normal' return type.
    0x38   :'error'    # [0x38, 0x01, errorcode, cksum]

    }

### Global hash table mapping error codes from drive to error messages
errortable={0x00: 'Normal (no error)',
            0x10: 'File does not exist',
            0x11: 'File exists',
            0x30: 'No filename',
            0x31: 'Dir search error',
            0x35: 'Bank error',
            0x36: 'Parameter error',
            0x37: 'Open format mismatch',
            0x3f: 'End of file',
            0x40: 'No start mark',
            0x41: 'CRC check error in ID',
            0x42: 'Sector length error',
            0x44: 'Format verify error',
            0x46: 'Format interruption',
            0x47: 'Erase offset error',
            0x49: 'CRC check error in data',
            0x4a: 'Sector number error',
            0x4b: 'Read data timeout',
            0x4d: 'Sector number error', # Again?
            0x50: 'Disk write protect',
            0x5e: 'Un-initialized disk',
            0x60: 'Directory full',
            0x61: 'Disk full',
            0x6e: 'File too long',
            0x70: 'No disk',
            0x71: 'Disk change error',

            # The following errors I've received from my PDD2, but
            # didn't see in any documentation

            0x80: 'Hardware failure', # (Belt got stuck in motor in my case)
            0xd0: 'Invalid filename'  # (Sent FIND an invalid filename request)
            }            

### Mapping of bits in 'condition' byte from drive to messages
conditiontable={
    0b1000: {0: "Power is normal", 8: "Low batteries"},
    0b0100: {0: "Disk is writable", 4: "Write protected"},
    0b0010: {0: "Disk in drive", 2: "No disk in drive"},
    0b0001: {0: "Disk not changed", 1: "Disk changed"}
    }


import serial
from serial.tools.list_ports import comports

def pickaport():
    portlist=[]
    for i,(port,desc,hwid) in enumerate(sorted(comports())):
        print "%d) %s (%s, %s)" % (i,port, desc, hwid)
        portlist.append(port)

    reply=raw_input("Type the # which the PDD is attached to or hit ENTER for " + portlist[-1] +": ")
    try:
        r=int(reply)
        port=portlist[r]
    except ValueError:
        if reply=="":
            port=portlist[-1]
        elif reply.startswith('/'):
            port=reply
        elif reply.startswith('tty'):
            port="/dev/"+reply
        else:
            port="/dev/tty"+reply

    print "Using '%s' as the port name to open." % (port)
    return port


def space(x):
    if type(x)==int:
        return ' '*x
    else:
        raise TypeError

def checkdsr():
    global ser                  # In case we have to reopen it when DSR is down but CTS is up.
    if not ser.getDSR():
        print "DSR is false, Please connect the Portable Disk Drive 2 and turn it on"
        if ser.getCTS(): 
            print "(By the way, oddly enough CTS is true so *something* is hooked up...)"
            print "Trying to reset the circuit by reinitializing the serial port"
            ser.close()
            ser = serial.Serial(port, baud, timeout=timeout, rtscts=True,dsrdtr=True)
        while not ser.getDSR():
            pass
        print "Okay, got DSR. Here we go!"

def checkcts():
    if not ser.getCTS():
        print "CTS is False... waiting for PDD to catch up"
        while not ser.getCTS():
            pass
        print "Okay, got CTS."



def write(s):
    checkdsr()
    checkcts()
    purgereadbuffer()

    print "SENDING: ", [ x if x.isalnum() else hex(ord(x)) for x in s ]
    ser.write(s)

def read(timeout=5):
    '''Read and interpret response from the drive. Optional timeout
    specifies how many seconds to wait for the first byte to be read.
    A timeout of -1 means "wait forever for the first byte" and might
    be useful for a long running task such as "format". After the
    first byte, the timeout is not used, but the serial module has its
    own read timeout (which we don't adjust here since it would slow
    down every read, not just the first).
    '''

    checkdsr()                  # XXX Should I be doing this? Why does the drive sometimes put DSR low but CTS high? USB<->Serial converter problem?


    print "RECEIVING"
    msg=""                      # Slowly accumulate data into "msg"

    # The drive may take some time before the first byte arrives, so
    # keep trying up to 'timeout' seconds.
    from time import time
    start=time()
    x=ser.read()
    while (x=="" and (time()-start<timeout or timeout==-1)):
        x=ser.read()    

    if x=="": return            # Drive has nothing to say...

    # First byte, "rt", specifies the "return type", how the following
    # data should be interpreted. In short:
    # READ FILE: 	(0x0a, len, data, cksum)  # (if len==0x80, read again)
    # FIND REFERENCE: 	(0x11, 0x1c, filename, size, free, cksum)
    # NORMAL RETURN:	(0x0c, 0x01, errorcode, cksum)
    # DRIVE CONDITION:	(0x0f, 0x01, condition, cksum)
    # (Bonus return type: 0x38, possibly for incorrectly formatted requests?)
    rt=ord(x)
    msg+=x
    if rt in returntable:
        print "Return type:", returntable[rt]
    else:
        print "Error! Unknown return type: ", rt
        purgereadbuffer()
        return
    
    # Second byte, "length", specifies number of bytes of data that
    # follow, not including the one byte checksum.
    x=ser.read()
    if x=="":
        print "Error! Only one byte received from drive."
        return
    length=ord(x)
    msg+=x

    # Now that we have the length, read that many bytes into "data"
    data=ser.read(length)
    msg+=data
    print "     length: ", length
    print "       data: ", [ x if x.isalnum() else hex(ord(x)) for x in data ]

    # Finally, read the one byte checksum
    x=ser.read()
    if x=="":
        print "Error! Ran out of data reading from drive." 
        return
    else:
        cksum=ord(x)


    # DONE WITH READING, NOW COMES INTERPRETATION...

    valid=iscksumvalid(msg, cksum)
    print "   checksum:", cksum, "(valid)" if valid else "(INVALID)"

    # Handle different return types differently

    if returntable[rt] == 'read':
        '''
        A chunk of the file is returned in "data", but it might be
        incomplete. If "length" is 0x80 (128 bytes), another READ
        command must be sent to drive to get the next chunk. (Which
        could be empty.)
        '''
        # Already printed data above, so not much to do here.
        if length==0x80:
            print "There is more data to read. Send another read command."

    elif returntable[rt] == 'find':
        '''
        Data is 28 bytes: 
          24 bytes filename, 
           1 byte attribute,
           2 bytes size, and
           1 byte free sectors. 

        Filename is "123456.89" padded with spaces to 24 characters.
        The period is always in the seventh position. If the filename
        is shorter than six characters, it is left-padded with spaces.

        The attribute byte is 'F' if a valid file response is being
        returned. If the directory is empty or no more files are to be
        found, the attribute is '\0'.

        The size is two bytes BIG endian. (MSB LSB)
        
        The number of free sectors should be multiplied by 1280 for bytes.
        '''
        attribute=data[24]
        if attribute=='F':
            filename=strip(data[0:24])
            size=256*ord(data[25])+ord(data[26])
            free=ord(data[27])
            print "filename:", filename
            print "size:", size, "bytes"
            print "free sectors:", free, "(", free*1280, "bytes )"
        elif attribute=='\0':
            print "No more files in the directory." 
        else:
            print "Error! Unknown attribute:", hex(ord(attribute))

    elif returntable[rt] == 'normal':
        '''
        I don't know why some documentation called this "normal" as
        only if the data is 0x00 was the condition normal. All other
        results specify error codes (as a single byte, easily looked
        up in a hash table). Any of the drive commands (except
        CONDITION) could potentially return this type of result. 
        '''
        d=ord(data)
        if d==0:
            print "Success"
        else:
            print " Error code:", errortable[d] if d in errortable else hex(d)

    elif returntable[rt] == 'error':
        '''
        Undocumented return type. 0x38 is the return type my PDD2
        replied with whenever I sent it a 'find' with an invalid
        length of data. (E.g., filename not padded to 24 bytes). It
        appears to be a variant of the 'normal' return type, as the
        length is 1 and the errorcode returned is "parameter error"
        (0x36) which makes sense.
        '''
        d=ord(data)
        if d==0:
            print "Success"
        else:
            print " Error code:", errortable[d] if d in errortable else hex(d)

    elif returntable[rt] == 'condition':
        '''
        The "condition" return type is a single byte bitmap of four
        flags in the low order bits. Assuming 1 means true, they are:
          0b1000 Low battery?
          0b0100 Write protected?
          0b0010 No disk in drive?
          0b0001 Disk changed?
        '''
        d=ord(data)
        i=1<<3
        while i:
            print "Drive condition:", conditiontable[i][d&i]
            i>>=1
    else:
        print "This should never happen. Unhandled return type:",returntable[rt]

    print



def purgereadbuffer():
    '''Before writing, we should make sure the drive isn't still
    trying to send bytes or our return results will get all
    higgledy-piggledy. This can happen if the program miscalculates
    the data length or if there is line noise. 

    Likewise, if we notice something going wrong while reading, we should
    clear out the read buffer.'''
    oldt=ser.getTimeout()
    ser.setTimeout(1)
    x=ser.read()
    if x!="":
        print "Purging excess data from drive: ",
    while x != "":
        print x, ord(x)
        x=ser.read()
    ser.setTimeout(oldt)
    

def checksum(s):
    '''Calculate the Tandy Portable Disk Drive checksum. Given the
    command string (without the leading "ZZ"), this routine returns
    the checksum as a single byte string.'''

    return chr((sum(map(ord,s)) % 256) ^ 255)

def iscksumvalid(s, c):
    '''Given a string and a checksum as an integer, returns
    True if the checksum is valid or False otherwise.'''
    return checksum(s)==chr(c)

def makecommand(opcode, data="", mode=None, bank=0):
    '''Given an opcode as an integer (and optionally a string of data
    and an integer for mode), returns a string ready to be sent over
    the serial port to the drive as a message. It does this by
    concatenating the preamble ("ZZ"), the opcode, the data length,
    the data, the optional mode, and finally the checksum.

    Optionally, opcode can be specified as a string, such as "open", 
    which will be automatically converted to the proper integer.'''

    if len(data)>255:
        print "Tandy's drive protocol does not handle data longer than 255 bytes."
        
    if type(opcode)==str:
        opcode=optable[opcode]

    if bank==1:                 # Second bank available on PDD2
        opcode=opcode|0x40


    if mode==None:
        mode=""
    elif type(mode)==str:
        mode=chr(modetable[mode])
    elif type(mode)==int:
        mode=chr(mode)

    if (opcode==optable['find'] or opcode==optable['delete']):
        attribute="F"
    else:
        attribute=""
    

    if (opcode==optable['find']): 
        data=alignfilename(data)        # Align to 24chars wide, period at 7.
 
    data=data+attribute+mode
    message=chr(opcode)+chr(len(data))+data
    preamble="ZZ"
    return preamble+message+checksum(message)


def alignfilename(s):
    '''Given a filename as a string, do some sanity checks and return
    the filename aligned with the period at the 7th position and
    padded with spaces to become 24 characters long. As a special case,
    if the input is the empty string, a string of 24 spaces is returned.'''

    if len(s)>24:
        print "Warning: Requested filename is longer than the Portable Disk Drive can handle."

    if len(s)>0:
        if s.count('.')==1:
            # Shift filename over so period is at position 7
            s="%6s.%s" % tuple(s.split('.'))
            if s.find('.')>6:
                print "Warning: Requested filename is longer than Model T's expect."
            if len(s)-1-s.find('.')>2:
                print "Warning: Requested extension is longer than Model T's expect."
        elif s.count('.')==0:
            # No period in filename. Is this legal?
            print "??? Will find on a filename without a dot work?" 
            print "??? Or should this program add one for you? FIXME." 
        else:
            print "Warning: Requested filename has more than one period."

    # Pad filename to 24 characters
    s="%-24s" % s

    return s


### MAIN

import argparse

parser=argparse.ArgumentParser(description='Control a Tandy Portable Disk Drive 2 from GNU/Linux')
parser.add_argument('-p', '--port', help='Port to connect to. Examples: /dev/ttyUSB0, ttyS0, S1. Default is last listed.')
parser.add_argument('-b', '--baud', help='Baudrate. Examples: 19200, 9600. Default is 19200.')
parser.add_argument('-t', '--timeout', help='Read timeout in seconds. E.g, 0, 10. (Default is 1).', type=int) # Probably should get rid of this as it's not actually useful.
args=parser.parse_args()


if args.port==None:
    port=pickaport()
else:
    if args.port=="":
        port=pickaport()
    elif args.port.startswith('/'):
        port=args.port
    elif args.port.startswith('tty'):
        port="/dev/"+args.port
    else:
        port="/dev/tty"+args.port

    # Should probably check here if port even exists. Ah well.

baud=args.baud if args.baud else 19200
timeout=int(args.timeout) if args.timeout!=None else 1

try:
    ser = serial.Serial(port, baud, timeout=timeout, rtscts=True,dsrdtr=True)
    print ser

    print "Carrier Detect "+str(ser.getCD())
    print "Clear to Send "+str(ser.getCTS())
    print "Data Set Ready "+str(ser.getDSR())
    print "Ring Indicator "+str(ser.getRI())
    print "RTS/CTS flow is set to "+str(ser.getRtsCts())
    print "DSR/DTR flow is set to "+str(ser.getDsrDtr())


    floppytest="ZZ"+chr(7)+chr(7)+chr(11)+chr(12)+"FLOPPY"
# Valid results for FLOPPY test when no disk is in the drive:
#    q{ for the one-bank Portable Disk Drive (PDD).
#    p| for the two-bank Portable Disk Drive (PDD2). 
# Note floppytest is equivalent to makecommand('status',data=chr(11)+chr(12)+"FLOPP")

    condition=makecommand('condition')

    # Some sample commands from the "PDD Command Reference"
    status="ZZ"+chr(7)+chr(0)+chr(248)+chr(13)
    seek1="ZZ"+chr(1)+chr(1)+chr(1)+chr(252)
    seek2="ZZ"+chr(1)+chr(1)+chr(2)+chr(251)
    seek3="ZZ"+chr(1)+chr(1)+chr(3)+chr(250)
    dir1="ZZ"+chr(0)+chr(26)+space(24)+"F"+chr(1)+chr(158)
    dir2="ZZ"+chr(0)+chr(26)+space(24)+"F"+chr(2)+chr(157)



    # Just a bunch of commands to test out the drive.
    write(makecommand('condition'))
    read()
    print "status"
    write(makecommand('status'))
    read()
#    print "format"
#    write(makecommand('format'))
#    read(timeout=-1)
    print "find reference"
    write(makecommand('find',data='MISTER.DO',mode='reference'))
    read()
    print "open for write"
    write(makecommand('open',mode='write'))
    read()
    print "write"
    write(makecommand('write',data='Lorem ipsum delorem lemur est.'))
    read()
    print "close"
    write(makecommand('close'))
    read()
    print "find first file"
    write(dir1)
    read()
    print "open for read"
    write(makecommand('open',mode='read'))
    read()
    print "read"
    write(makecommand('read'))
    read()
    print "find first file"
    write(makecommand('find',data='',mode='first'))
    read()
    print "find next file"
    write(makecommand('find',data='',mode='next'))
    read()
    print "find next file"
    write(makecommand('find',data='',mode='next'))
    read()

    ser.close()

except serial.SerialException as e:
    print "pdd2.py: Failed to open" + port
    print e

Reply via email to