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