file pfkerm.txt This is a conversion of the Forth block file KERMIT.DOW so as to be readable as a plain text file. The complete package, including the actual block files, is available from my web site as http://www.eskimo.com/~pygmy/pfkerm.zip This pfkerm.txt and KERMIT.DOW provide the "shadow" comments to match the source code in the file pfkerm.src and KERMIT.SCR. KERMIT.SCR Contains a simple implementation of the Kermit file transfer protocol. copyright 1997 Frank Sergeant pygmy@pobox.com 809 W. San Antonio St. San Marcos, TX 78666 This source code is not Public Domain or Shareware. You may use it freely for any private or commercial purpose provided you do so at your own risk. ( load screen Kermit file transfer protocol) For the algorithm, see pp 98-113 of _C Programmer's Guide to Serial Communications_ by Joe Campbell, Howard W. Sams & Company, 1987, ISBN 0-672-22584-0. Note, there are some errors in Campbell's examples. ( KERMIT) GET-Y/N Wait for the user to press a y, n, Y, or N key. Return true if y or Y. Return false if n or N. TRY-AGAIN? Display a message and ask whether the user wants to try again. E.g. " Drive not ready" TRY-AGAIN? IF ... THEN .MSG clears the top 2 lines of the screen and displays a message, leaving the cursor positioned just past the message. E.g. " Starting the transfer ..." .MSG ( KERMIT) MYMAXL maximum "len" we are willing to handle. The transmitted LEN field includes SEQ, TYPE, DATA, CKSUM fields. 94 maximum allowed under basic Kermit. Our buffers must be 1 byte longer to hold the LEN field also. OUT-BUF & IN-BUF buffers for building outgoing or receiving incoming frames. We store LEN, SEQ, TYPE, DATA, CKSUM fields, but not the SOH nor the ending CR. OUTLEN & INLEN count bytes currently in the buffers MAXL holds agreed-upon maximum "len" value, which is the MIN of receiver's and sender's preferences. a "character-ized" number is produced by adding a "space." The result must be <= $7E, thus the original number must be <= $5E (ie 94 decimal). ( KERMIT) MAXL, QCTL, etc are the agreed-upon protocol parameters for the session. INIT-LIMITS initializes these to the values we would prefer to use. The sender and receiver exchange an S-frame and an ACK-frame listing their preferences. We then compromise by taking the MIN between them. ( KERMIT) We make >LEN, >TYPE, etc relative to the start of the buffer so we can use the same definitions for both the receiving and sending buffers. >CKSUM assumes the LEN byte has been initialized. ( KERMIT - compromise on the parameters) COMPROMISE assumes we have an S frame in one buffer and its ACK frame in the other buffer. We don't care whether we are the sender or receiver. The compromise takes the more conservative setting from each buffer as the actual protocol parameter to use. For now, we will ignore all the settings except for MAXL and TIMEOUT, taking the MIN of MAXL and the MAX of TIMEOUT. MYMENU cheap error handling in the case where the user chooses to abort the file transfer process. Set up your own menu ( ' MYREALMENU IS MYMENU ) or allow the default 'no vector' error to occur. KSER-IN gets a serial character and tests whether it is SOH, all the while checking for a time-out. Returns character and SOH-flag (true if character is SOH). In case of time out, return up an extra level, putting a 'V on the stack as the dummy frame type indicating a time out followed by a true flag indicating a 'good' check sum. Note, KSER-IN is only called by GETFRAME and so is always called with the correct stack depth. To test it standalone, nest it once in a test word, as shown in TEST-IN. ( KERMIT) We "controlify" a control code (0-$1F, $7F) by flipping bit 6 and preceding it with the QCTL character (usually '#). The QCTL character itself is escaped. We count QCTL as a control character in CTRL? so we can escape it, but we only flip bit 6 for actual control characters. Also, consider $7E (~) to be a control character, as it is used for repeat counts (KEMIT puts a character into OUT-BUF and increments the count KEMIT writes a character into OUT-BUF, escaping it if necessary. ROOM? says whether there is room in the buffer for another character. We require 2 bytes available in case the next character needs to be escaped. If we allowed high-bit escpaping we would require 3 bytes instead. ( KERMIT) CK%% converts the raw checksum of all the bytes after SOH into a checksum character by wrapping and characterifying it according to the KERMIT algorithm. CKSUM calculates a checksum on a buffer by adding the bytes in the LEN SEQ TYPE & DATA fields and applying CK%%. The LEN field must include the cksum byte. CKSUM? Calculate the checksum character for the input frame and compare it to the transmitted checksum character. Return true if the checksum is good. MODEM! sends a character to the modem. We defer it to make testing easy. DATA! builds an entire data field, stopping either when out of source characters or out of room in OUT-BUF. BUILD-FRAME Given the address and length of data to be transferred and the type of the frame, put as much of the data as will fit into a frame and return the address and length of the remaining (i.e. unsent) data. ( KERMIT - debugging aids) .FRAME .INB .OUTB are used for testing to dump the contents of the buffers to the screen. TEST1 TEST2 provide some test data ( KERMIT) SENDFRAME sends an entire header, from SOH through 1-byte checksum and ending carriage return, to the "modem." It sends SOH, sends LEN+1 characters in the OUT-BUF, and then sends a carriage return. ( KERMIT) LIMITS provides data for use in building either an S-frame or its ACK frame for purposes of negotiating the protocol as to maximum frame length, etc. Note that PADC is controlified, but seems not to be "escaped" -- after all, we haven't agreed upon the escape character at the time of sending the S-frame. We build this frame directly into OUT-BUF to prevent DATA! from escaping any characters. We say we'll use (~) as the repeat character, but we will _not_ use repeat counts when we transmit, but we _will_ handle them when we receive. If the sender does not escape actual tildes, then we will have a problem. ( KERMIT) KINIT sends the 'send-init' frame. It must have sequence zero. This is the 'S' frame sent by sender in response to the receiver's initial NAKs. KINITACK sends a reply to a 'send-init' frame. Before sending KINITACK (if we are receiving) or after receiving KINITACK (if we are sending), we must adjust our settings to the minimum of the sender's and the receiver's requests Note complex handling of COMPROMISE. FILEHEADER sends the file name of the file to be transmitted. EOF is sent at the end of each file we send. EOT is sent after we finish sending all the files. Reciever sends ACK or NAK after each frame is received, depending on whether chksum is ok. ERROR is sent to abandon the session. I think we will ignore an ATTRIB frame. ( KERMIT) EXPECTED holds the count of bytes we expect to receive following the length byte. SETLENGTH handles the length count for an incoming frame, initializing EXPECTED and INLEN and putting the length byte into the input buffer. PUT-IN-BUFFER puts input bytes into the buffer and returns a flag that is true when all the expected bytes have arrived. GETFRAME is closely tied to KSER-IN and is the only word that should ever call KSER-IN, as KSER-IN returns upward an extra level in case of a timeout, supplying the type and cksum flag (ie 'V -1). So, GETFRAME always succeeds, returning a type and flag. It watches for an SOH in the middle of a frame and starts over. What makes GETFRAME tricky is it needs to handle the usual case as well as a timeout at any time as well as an unexpected SOH at any time. What makes it simpler is pushing some of the logic down to the word KSER-IN and letting KSER-IN terminate not only itself but also GETFRAME in the case of a timeout, thus producing a dummy V-frame. After that we no longer have a timeout as a special case, we simply have an additional "frame" type (i.e. a timeout frame). ( KERMIT) GET-GOOD-FRAME continues to try to get a frame until one arrives with a good checksum. It will try forever unless the user aborts the transfer. (See KSER-IN for test for user abort.) IN-SEQ sequence number of the frame in the input buffer GOOD-SEQ? true if the input frame's sequence number is the expected sequence number. ( KERMIT) (GETACK keeps getting frames until one comes in with a good checksum. V-frames are ok. GETACK keeps getting ack frames, handling or ignoring each, as appropriate. It re-sends the data frame in case of a V-frame (timeout) or a NAK with the correct sequence number. It is used only by the sender. Later, it could bail out if too many NAKs or timeouts occur in a row, etc. READ load up the buffer from the file in preparation for transmitting it via the serial port ( KERMIT) GET-FIRST-NAK ignores timeouts and sequence numbers and waits for a NAK from the receiver. SEND wait for the prompting NAK frame from receiver send S-frame ( ie KINIT) reset serial in to throw away any extra prompting NAKs get S-frame ACK for SEQ 0 send the entire file, one D-frame at a time close the file send end of file and end of transmission ( KERMIT) IN-DATA is a buffer for holding the UNCTRL'd data field. Make it big in case lots of repeat counts are present. C!+ stores a character and bumps the address (similar to C@+) C@+- gets a character from the 'from' address, increments the 'from' address and decrements the count of remaining characters. UNCTRL'd if the current character is the QCTL escape character, get another character and unescape it. REPEAT'd The most recent character was the tilde (~), indicating the beginning of a 3 or 4 character repeat sequence. Get the next character as the count and then the next 1 or 2 (if escaped) to find the value to be repeated, & expand that repeated character into destination buffer. UNCTRL copy the escaped and repeated source buffer, unescaping and expanding as appropriate, to the destination buffer. >IN-DATA copies IN-BUF's data field, which may contain escaped characters, to IN-DATA with escaped characters converted to their actual values (and repeated counts expanded). ( KERMIT) BUILDFNAME extracts name of file to be received from an input F frame and stores it in our KFNAME buffer as a counted string (and an asciiz string suitable for passing to DOS for creating the file). RCVNAME this is what we do in response to an F-frame: save the file name in the KFNAME buffer as a counted, asciiz string, then create the file and save the handle. ( KERMIT) GET-NEXT Get the next frame we are expecting, ACKing or NAKing as appropriate. Always ack with the seq number we received, even if it wasn't the seq number we expected, thus allowing sender to continue. But, throw away frames that do not have the expected seq number. Except, if V-frame (ie timeout) or if a bad checksum, then NAK with our expected sequence number. It is possible a D-frame should not be ACK'd until after we have written it to disk, in case disk writes interfere with servicing the serial port. WRITE Append input data to the file. ( KERMIT) RECEIVE send NAK every second until we see SOH, then get the rest of that first frame -- until we get the S-frame. Then compromise on settings and send an ack for the S-frame. Then, handle the frame types, getting file name and opening it for an F-frame, writing D-frames to the file, closing the file upon getting a Z-frame, and exiting upon getting a B-frame (EOT).