Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Tools

Kermit Meets Modula-2


MAY89: KERMIT MEETS MODULA-2

KERMIT MEETS MODULA-2

The modularity of Modula-2 makes it well-suited for communications projects

Brian R. Anderson

Brian is an instructor in the computer systems technology department of the British Columbia Institute of Technology. He can be reached at BCIT, Burnaby Campus, 3700 Willingdon Ave., Burnaby, BC V5G 3H2 Canada.


Kermit (named after the famous frog) is a general-purpose file transfer protocol that was developed at Columbia University in 1981. What is innovative about Kermit is that it makes minimal assumptions about the hardware upon which it operates. While this innovation allows Kermit to be used on virtually any computer, Kermit is typically used for communications between microcomputers and larger computers (minicomputers or mainframes).

This article describes a minimal implementation of Kermit that uses Logitech Modula-2. In order to provide a framework for my discussion of the design and implementation of the program, I will first discuss the Kermit protocol in some detail. I will use the layered protocol approach to describe how I employed a Yourdon-style Data Flow Diagram (DFD) for the overall design of the program. Finally, I will discuss how the use of Modula-2 allowed me to implement each layer of the protocol as a separately compiled module.

The Need For Data Communications Protocols

On the surface, the process of data communications appears to be very simple: Data is converted into a form that matches the transmission medium (the MODulator part of the Modem handles this step); the data is sent on its way; and then the data is converted back to the original data at the other end (this step is the task of the DEModulator part of the Modem). Unfortunately, the actual process is seldom that easy.

If the transmission path used to move data were perfect, no communication protocol would be necessary (or at least, the protocol could be very simple), and the data could merely be sent as electrical impulses from one system to another. All real transmissions paths suffer from problems such as noise, dropout, and distortion (including phase distortion, frequency distortion, and amplitude distortion). Despite the use of the best possible hardware, any of these impairments can cause data errors -- such that the received message does not match the transmitted message. To be effective, a file transfer protocol must detect such errors and take corrective action.

The receiver and the transmitter may not have the same timing requirements. If the receiver is not ready for data when the transmitter starts sending, information will be lost. A file transfer protocol must also allow the receiver and the transmitter to synchronize so that such data loss does not happen.

The Kermit Protocol

Kermit is a point-to-point (as opposed to multipoint) communications protocol that uses stop-and-wait Automatic Retransmission reQuest (ARQ) for error control. (XModem, which is widely used for communications between microcomputers, is also a point-to-point, stop-and-wait protocol.) After sending some data, Kermit stops and waits for an acknowledgement of the data that it just sent. If no acknowledgement (or a negative acknowledgement) occurs, Kermit resends any data that had been sent since the last acknowledgement.

Kermit handles all communications via packets (unlike XModem, which uses single characters for acknowledgement). A packet is a stream of bytes that fits a particular pattern. Figure 1 shows a block diagram of the Kermit packet.

The Mark field uniquely identifies the beginning of the Kermit packet. This field contains an ASCII start-of-header character, which is the only nonprintable character anywhere in the packet. (If nonprintable characters are encountered elsewhere, they are converted into printable characters. This process is explained shortly.)

The Length field contains a count of the number of characters in the rest of the packet; this count ranges from 3 (to indicate an empty data field) to 94 (the maximum size of a data field). The Sequence field indicates the packet sequence number. The range of sequence numbers is from zero to 63. (After 63, the sequence repeats.) The Type field identifies the type of packet. Packet types include D (data), Y (yes, acknowledge), N (negative acknowledge), S (send initiate), F (filename), Z (end of file), E (error), and B (break transmission, end of transmit). (There are other packet types that are not used in this implementation; see the references given at the end of this article for further details.)

The Data field in the Kermit packet contains just what its name implies -- data. (Due to the constraint that the packet contain only printable characters, some data must be converted to printable form and/or encoded.) Finally, the Check field (which is usually an arithmetic CheckSum) allows error control.

The features of Kermit that allow this protocol to be used on nearly any computer system are the small packet size (96 characters maximum) and Kermit's reliance upon printable characters. The latter is necessary because many systems interpret nonprintable characters in special ways. (In Unix, for example, Control-D means end-of-file; in CP/M and MS-DOS, Control-Z may be used for the same function.) In addition, many mainframes can deal with only 7-bit characters -- these mainframes cannot use the parity bit for data. In order to avoid the problems caused by control and binary characters, the designers of Kermit choose to convert any suspect characters into normal printable characters. In this context, suspect characters include any control characters that have a binary 1 in the eighth bit.

Initialization

Before transmission can begin, the receiver and the transmitter must agree upon certain parameters. Some of these parameters (transmission rate, parity, and the number of stop bits) must be set up in hardware before Kermit can begin. Other parameters are handled via a limited negotiation that occurs during transmission of the first packet. The data field of the first packet contains several fields for this purpose. This negotiation process is very simple: If there is no agreement about a feature where agreement is necessary, an automatic fallback to defaults occurs. When agreement about a feature is not necessary, each end complies with the other end's wishes.

Some of the fields within the initialization field may contain values that do not result in printable ASCII characters. Each of these values must be converted by adding 20H to the value. This process, called "character-izing," is performed on the MAXL, TIME, NPAD, and EOL fields. The PADCfield is treated differently. This field is "control-ified," which means that bit 6 is inverted, by XORing it with 01000000B. Other fields are sent as literal characters.

Figure 2 shows the contents of the initialization packets. (One packet is sent from each end.) During initialization, each end initializes the other. (The pronouns "I" and "you" are often used to describe each end of the communications link.) Table 1 describes each of the fields in the initialization packet.

Table 1: Fields in the initialization packet

  FIELD  FUNCTION
  ---------------

  MAXL   The maximum packet size that I can accept; you will also
         indicate your maximum packet length.
  TIME   The maximum time (in seconds) that you should wait before timing
         me out.
  NPAD   The number of padding characters that you should send ahead of
         each packet.  (Default is zero.)
  PADC   The padding character that you should use.
  EOL    The character that you should send to terminate each packet
         (usually none or ASCII <cr>).
  QCTL   The character that I will use to control the process of
         character quoting (usually #).
  QBIN   The character that I will use for quoting binary bytes
         (usually &.
  CHKT   The type of check character used: 1 means a one-byte CheckSum;
         2 means a two-byte CheckSum; and 3 means a three-byte CRC.
         This version uses only the one-byte CheckSum.
  REPT   The character that I will use for repeat-count encoding.  A blank
         (ASCII <sp>) means that this feature is not used (as is the case
         with this implementation).
  CAPA   Advanced capabilities.  Each bit has a separate function, and
         several bytes may be linked together (bit zero is a linkage
         bit).  These features are not used in this implementation.

File Transfer

After initialization, the process of file transfer consists of sending or receiving any number of files. Each of these files is prefixed with a packet that contains the filename and terminates with an end-of-file packet. After all files have been transferred, an end-of-transmit packet is sent.

While this implementation cannot transmit multiple files (each file must be transmitted with a separate Send command), it can receive multiple files.

Figure 3 and Figure 4 show state diagrams for the processes of receiving and sending, respectively (adapted from reference 1 in the bibliography following this article). These simplified diagrams are meant only to present an overview of the sequence of events that occur during file transfer.

As mentioned earlier, Kermit requires that all data be sent as printable characters. To accomplish this, and at the same time to allow any type of data to be transmitted, control codes (ASCII 00H - 19H and 7FH plus binary codes outside of the range of ASCII 80H - FFH) must be handled in a special way. To do so, the data is modified so that it is a printable character, and a "prefix" (so-called "quote") character is used to advise the other side that this modification has been performed.

Control characters are XORed with 01000000B (which inverts bit 6), and then prefixed with #. For example, a <cr> <lf> sequence (which is Control-M and Control-J) becomes #M#J.

When a character has a binary 1 in the most significant bit position, this bit is inverted and the resulting character is prefixed with &. For example, 11000001B is converted to &A.

In some cases, both of these quoting schemes must be used: 10000001B becomes &#A. The two prefix (quoting) characters can be sent by prefixing them with #. # becomes ##, while & becomes #&. The use of quoting schemes can add considerable overhead. The control characters make up 26.6 percent of the ASCII character set, while bytes with the most significant bit high make up 50 percent of random binary files. In contrast, text files can be transmitted quite efficiently because they contain few control characters and no nonASCII characters.

Layered Communications Protocols

Communications protocols are often described as having several layers (or levels). Each of the layers is responsible for certain aspects of the steps involved in establishing and maintaining a connection, moving the data, or ensuring data integrity. Layered protocols allow a standard to be flexible enough to be widely adopted. The inherent modularity of layered protocols also makes the process of implementing standards easier.

The ISO-OSI Seven-Level Model

The most notable layered protocol is the International Standards Organization's Open Systems Interconnect Seven-Level Reference Model, usually called the OSI model. This was meant to be a universal model for large-scale international networks, although many of the principles also apply to more limited applications (such as Kermit).

The diagram in Figure 5 compares the OSI model with Kermit. Because Kermit is a simple point-to-point protocol where the connection process is under the manual control of the user, not all of the OSI layers are required.

Software Design

By following the data flow through the protocol layers, design of the program becomes straightforward. As mentioned at the beginning of this article, I used a data flow diagram (DFD) as an initial design tool. The DFD, which is shown as Figure 6, provides a clear picture of the overall design of this Kermit implementation.

When designing a program in Modula-2, an early step (and the next step here) is to design the main module and the definition modules. The main module, called the PCKermit, calls the other modules. Definition modules contain no code, and describe the actions the implementation modules will perform. Four definition modules were designed from the DFD: Shell, PAD, Files, and DataLink. The physical layer module is provided by the Modula-2 compiler that I used (Logitech), so some effort was saved here. The compiler-supplied module that implements the physical layer is named RS232Int (for "interrupt-driven access to the RS-232 serial port"). The main module and the definition modules are shown as Listings One through Five respectively.

The definition modules closely match the DFD, with the exception that the packet assembler and the packet disassembler are combined into a single module, called PAD (for Packet Assembler/Disassembler). Although many procedures are hidden within the implementation of PAD, only two are imported and used by the main module: Send and Receive. The main module imports several procedures from Shell, including dispOpts, Options, Dir, Connect, eXit, and MainHelp. The purpose of many of these procedures is obvious from their names: dispOpts and Options refer to the communications options (Baud rate, parity, and so forth). Connect provides terminal emulation.

The Files, DataLink, and RS232Int modules follow the layering concept and are not accessed from the main module. These lower-level modules are accessed only from within PAD.

Although examination of the main module and the definition modules reveals few (if any) details of the underlying implementation, it does provide a good picture of the overall design of the program.

Implementation Details

Each of the modules mentioned above is divided into several procedures, and each of these procedures has one well-defined purpose or task. Here are descriptions of a few of the key parts of the code:

The Shell Module

In Modula-2, each implementation module can have an initialization section, which is also called the "module body." The module body looks like a main part of a program module. It is executed once, when the program first starts up, in order to initialize the module. In the case of the Shell module, the module body sets the initial conditions of the communications hardware (that is, the Baud rate, parity, and so on).

The Shell implementation module (Listing Six) contains several local procedures for setting the Baud rate, parity, word length, and number of stop bits used by the communications hardware. In Modula-2, local procedures that are not mentioned in the EXPORT list of the definition module are available only within the module in which these procedures are defined. In this case, these procedures are accessed by the Options procedure, which is exported and used by the main module, PCKermit.

I had a bit of fun with the Dir procedure -- I wanted a single command that could either display the current directory, or else change to another subdirectory, and then display the contents of that subdirectory. This required some minor parsing of user input in order to separate the directory name from the file name. For example, if you entered FOO\*.EXE, the Dir procedure would try to switch to a subdirectory named FOO, and then display all of the .EXE files. Dir is smart enough to know the difference between FOO\*.EXE and \FOO\*.EXE. A useful enhancement would be the ability to log onto a different disk drive; this enhancement would just require a bit more parsing.

The Connect command emulates a glass teletype by scanning the keyboard for input and the sending any input that it receives it out to the RS-232 serial port. Connect also scans the RS-232 serial input port, and then sends anything that it receives to the screen. Some interpretation is necessary in order to prevent control characters from printing, to allow for backspaces, and to switch echo modes on and off. The most useful addition that could be made here would be full terminal emulation (to make the screen and keyboard act like a common video terminal, such as the Televideo 950 or the DEC VT100).

The Pocket Assembler/Disassembler (PAD) Module

Although the PAD module (Listing Seven) contains over 20 procedures, only two procedures (Send and Receive) are exported for the use of other modules. The balance of the procedures (some of which are only a few line of code) help the exported procedures get the job done.

Several of the procedures construct or decipher the various control packets, such as the initialization, response (ACK/NAK), and error packets.

The Send procedure reads data from a file and uses that data to construct a packet. Before a character is added to a packet, the character must be checked to see if it is a control character or if it has the most significant bit set. If either condition exists, the character is altered, and a quote character (# or &) is added. When the packet is nearly full, the count, sequence, and type bytes are added before the packet is passed on to the DataLink module.

The Receive procedure contains a nested loop. (Example 1 shows the pseudocode for this loop.) As required by the protocol, each successfully received packet is acknowledged by a Y packet. If no packet is received, or if the received packet contains errors, then an N packet is sent instead. After several errors in a row (errors = MAXtrys), file transfer is abandoned and an error message is issued to the user. If all goes as it should, each good packet is processed to remove both control-quoting and binary-quoting (e.g., #M#J is changed to <cr><lf> again, and so on) before the data is stored to the output file.

Example 1: Pseudocode for the Receive procedure

         WHILE <"end of transmit" packet not received> DO
            (* receive a file *)
       WHILE <"end of file" packet not received> DO
            (* receive a packet *)
         END;
     END;

Two small procedures, Char and UnChar, are used throughout this module to convert packet service bytes to/from their "characterized" format. (This is necessary to ensure that the sequence and length fields contain only printable characters, and that these characters are later correctly interpreted as numbers.)

The Files Module

The purpose of the Files module (Listing Eight) is two-fold: to provide a "nicer" interface to the underlying file system, and to gain full control of disk buffering.

Wirth's FileSystem module uses a single procedure, called Lookup, to open existing files or to create new files. (A Boolean parameter determines which action Lookup performs.) I have used Lookup to construct separate Open and Create procedures. The Open procedure returns a Status Error if the file does not exist. If the Create procedure determines that the requested file already exists, this procedure advises the user and asks if the file should be overwritten before it continues.

One of the major problems encountered when developing communications programs is timing -- specifically, it is possible to miss incoming data if the program is doing something that is time-consuming (such as disk I/O) at the "wrong" time. The "right" time to output to the disk is after a packet has been received, but before the packet has been acknowledged. The stop-and-wait protocol then ensures that no more data is received until after the disk write is complete.

The Files module writes to the disk in two stages. The Put procedure puts the character into a buffer. The DoWrite procedure outputs the buffer to the disk only if the buffer is nearly full (that is, if it is too full to fit into another packet). The Receive procedure in PAD calls Put for each character in the packet, and then calls DoWrite at the end of the packet. (At first glance, you might be tempted to use a block write command to store the entire packet that is received. This is not possible, however, because the packet contains a great deal of information that should not be stored to the file -- the service fields [Mark, Length, Sequence, Type, and Check] and the control/binary quoting characters must be stripped from the packet before it is saved.) Finally, the CloseFile procedure must ensure that the buffer is flushed to disk before actually closing an output file.

The DataLink Module

The DataLink module (Listing Nine) receives and transmits packets. The Send Packet procedure accepts a packet from PAD and writes the packet to the serial port, one character at a time (via the RS232Int module). The ReceivePacket procedure reads characters from the serial port (via RS232Int) and assembles them into packets, which are then passed on to PAD. SendPacket is by far the simpler of these two procedures -- it outputs the characters in a loop, calculates the CheckSum as it goes, and then outputs the CheckSum at the end. Very little can go wrong with Send Packet.

By contrast, the Receive Packet procedure has to be able to handle several potential problems: timeouts (no data received), packet format, and CheckSum errors. First of all, ReceivePacket looks for the Mark character (ASCII SOH) until one of the following three events occurs: ReceivePacket reads 100 characters without finding SOH; about 10 seconds elapses; or SOH is read. If SOH is not encountered, an error message is output to the screen and error status is returned to PAD, which decides whether or not to try again. If SOH is encountered, ReceivePacket then receives the rest of the packet. ReceivePacket also assumes that the next byte is the Length field, which is used to determine how many characters should be read before expecting the CheckSum field. If that byte is not the Length field, the CheckSum field will be wrong, and the error will be recognized. As the characters are being received, a local CheckSum is calculated for comparison to the CheckSum field that is received from the remote end. If the CheckSum do not match, an error is reported to the screen and to PAD. As always, it is vital that the Receive routines time out if no data is received.

Notice that although the CheckSum is a simple arithmetic sum of the ordinal value of the characters, some rather bizarre calculations are performed upon the sum before it is used. These calculations ensure that the resulting CheckSum is a printable ASCII character, and make all of the bits of the original sum significant (so that all of the bits have some effect upon the CheckSum field).

Performance

Kermit is most useful if no other file transfer method is available, as is often the case when dissimilar computers are involved. Kermit is relatively efficient when transferring text files. When Kermit is used to transfer binary files, its quoting schemes can add more than 70 percent redundancy.

I have used this implementation of Kermit to upload and download files to the IBM 3083 mainframe at the British Columbia Institute of Technology. This implementation has also been tested successfully with the Kermit section of Procomm, using a direct link (NULL MODEM) at up to 9600 Baud.

Portability

I have ported this implementation to the Atari ST, and now frequently use the two Kermits to transfer files between the ST and the PC. The only significant change in porting over to the ST involved writing an RS232Int module for the ST in order to handle differences in the interface to the serial communications hardware on the ST.

The process of moving Kermit to other platforms should involve minimal effort if a reasonably complete Modula-2 compiler is available. Of course, conversion to a different language is also possible, although that approach would involve considerably more effort. Several C implementations of Kermit are available on various bulletin boards and directly from Columbia University.

Limitations and Problems

This was meant to be a minimal implementation of the Kermit protocol, so none of the advanced features were incorporated. Anyone interested in adding these features is referred to the bibliography at the end of this article. (The Joe Campbell book includes some recent enhancements to the protocol that are not mentioned in the earlier Byte article, but the article is more detailed and complete.)

Conclusion

When implementing a communications protocol, it is vital to understand and to apply the concept of layering. Each layer has in the protocol a small number of well-defined tasks. If each layer is handled by an independent module, the protocol's overall complexity is broken down to a manageable level. The ISO-OSI Seven-Level Reference Model provides a useful model, even for simpler protocols such as Kermit.

The most subtle problems that can arise with a communications protocol are due to timing constraints -- one end will not quit sending just because the other end is busy doing something other than receiving. It is important to properly handle such potential problems in order to prevent data loss.

The Modula-2 programming language proved to be well able to handle this communications project. The design and implementation were completed in about a week. Due to the modular nature of the program, alterations and enhancements will not be difficult.

Bibliography

    1. de Cruz, Frank; and Catchings, Bill. "Kermit: A File-Transfer Protocol For Universities." Byte Magazine June/July 1984).

    2. Campbell, Joe. C Programmer's Guide to Serial Communications. Indianapolis, Ind.: Howard W. Sams & Company, 1987.

    3. McGovern, Tom. Data Communications. Concepts and Applications. Englewood Cliffs, N.J.: Prentice-Hall, 1988.

Availability

All source code for articles in this issue is available on a single disk. To order, send $14.95 (Calif. residents add sales tax) to Dr. Dobb's Journal, 501 Galveston Dr., Redwood City, CA 94063, or call 800-356-2002 (inside Calif.) or 800-533-4372 (outside Calif.). Please specify the issue number and format (MS-DOS, Macintosh, Kaypro).

_Kermit Meets Modula-2_ by Brian Anderson

[LISTING ONE]

<a name="00e6_0019">

MODULE PCKermit;

   FROM Break IMPORT
      DisableBreak, EnableBreak;

   FROM Terminal IMPORT
      WriteString, WriteLn, Read;

   FROM Shell IMPORT
      dispOpts, Options, Dir, Connect, eXit, MainHelp;

   FROM PAD IMPORT
      Send, Receive;


   VAR
      Quit : BOOLEAN;
      ch : CHAR;


BEGIN   (* main program *)
   DisableBreak;   (* don't recognize Control-C *)
   WriteLn;   WriteLn;
   WriteString ("Welcome to PCKermit -- Mainframe to Micro Communications");
   WriteLn;
   dispOpts;
   Quit := FALSE;
   REPEAT
      WriteLn;   WriteLn;
      WriteString ("PCKermit [O, C, D, S, R, X, ?]: ");
      LOOP
         Read (ch);
         CASE CAP (ch) OF
            'O' : Options;       EXIT;
         |  'C' : Connect;       EXIT;
         |  'D' : Dir;           EXIT;
         |  'S' : Send;          EXIT;
         |  'R' : Receive;       EXIT;
         |  'X' : eXit (Quit);   EXIT;
         |  '?' : MainHelp;      EXIT;
         ELSE
            (* ignore *)
         END;
      END;
   UNTIL Quit;
   EnableBreak;
END PCKermit.




<a name="00e6_001a"><a name="00e6_001a">
<a name="00e6_001b">
[LISTING TWO]
<a name="00e6_001b">

DEFINITION MODULE Shell;   (* User interface for Kermit *)

   EXPORT QUALIFIED
      dispOpts, Options, Dir, Connect, eXit, MainHelp;

   PROCEDURE dispOpts;
   (* Display communications parameters for the user *)

   PROCEDURE Options;
   (* set communications options *)

   PROCEDURE Dir;
   (* Displays a directory *)

   PROCEDURE Connect;
   (* Terminal mode allows connection to host (possibly through MODEM) *)

   PROCEDURE eXit (VAR q : BOOLEAN);
   (* Allow user to exit program after prompting for confirmation *)

   PROCEDURE MainHelp;
   (* help menu for  main program loop *)

END Shell.





<a name="00e6_001c"><a name="00e6_001c">
<a name="00e6_001d">
[LISTING THREE]
<a name="00e6_001d">


DEFINITION MODULE PAD;   (* Packet Assembler/Disassembler for Kermit *)

   EXPORT QUALIFIED
      PacketType, yourNPAD, yourPADC, yourEOL, Send, Receive;

   TYPE
      (* PacketType used in both PAD and DataLink modules *)
      PacketType = ARRAY [1..100] OF CHAR;

   VAR
      (* yourNPAD, yourPADC, and yourEOL used in both PAD and DataLink *)
      yourNPAD : CARDINAL;   (* number of padding characters *)
      yourPADC : CHAR;       (* padding characters *)
      yourEOL  : CHAR;       (* End Of Line -- terminator *)

   PROCEDURE Send;
   (* Sends a file after prompting for filename *)

   PROCEDURE Receive;
   (* Receives a file (or files) *)

END PAD.





<a name="00e6_001e"><a name="00e6_001e">
<a name="00e6_001f">
[LISTING FOUR]
<a name="00e6_001f">


DEFINITION MODULE Files;   (* File I/O for Kermit *)

   FROM FileSystem IMPORT
      File;

   EXPORT QUALIFIED
      Status, FileType, Open, Create, CloseFile, Get, Put, DoWrite;

   TYPE
      Status = (Done, Error, EOF);
      FileType = (Input, Output);

   PROCEDURE Open (VAR f : File; name : ARRAY OF CHAR) : Status;
   (* opens an existing file for reading, returns status *)

   PROCEDURE Create (VAR f : File; name : ARRAY OF CHAR) : Status;
   (* creates a new file for writing, returns status *)

   PROCEDURE CloseFile (VAR f : File; Which : FileType) : Status;
   (* closes a file after reading or writing *)

   PROCEDURE Get (VAR f : File; VAR ch : CHAR) : Status;
   (* Reads one character from the file, returns status *)

   PROCEDURE Put (ch : CHAR);
   (* Writes one character to the file buffer *)

   PROCEDURE DoWrite (VAR f : File) : Status;
   (* Writes buffer to disk only if nearly full *)

END Files.





<a name="00e6_0020"><a name="00e6_0020">
<a name="00e6_0021">
[LISTING FIVE]
<a name="00e6_0021">

DEFINITION MODULE DataLink;   (* Sends and Receives Packets for PCKermit *)

   FROM PAD IMPORT
      PacketType;

   EXPORT QUALIFIED
      FlushUART, SendPacket, ReceivePacket;

   PROCEDURE FlushUART;
   (* ensure no characters left in UART holding registers *)

   PROCEDURE SendPacket (s : PacketType);
   (* Adds SOH and CheckSum to packet *)

   PROCEDURE ReceivePacket (VAR r : PacketType) : BOOLEAN;
   (* strips SOH and checksum -- return FALSE if timed out or bad checksum *)

END DataLink.





<a name="00e6_0022"><a name="00e6_0022">
<a name="00e6_0023">
[LISTING SIX]
<a name="00e6_0023">


IMPLEMENTATION MODULE Shell;   (* User interface for Kermit *)

   FROM SYSTEM IMPORT
      AX, BX, CX, DX, SETREG, SWI;

   FROM Exec IMPORT
      DosCommand;

   FROM Terminal IMPORT
      WriteString, WriteLn, KeyPressed, ReadString;

   IMPORT Terminal;   (* for Terminal.Write and Terminal.Read *)

   FROM InOut IMPORT
      WriteCard;

   FROM RS232Int IMPORT
      Init, StartReading, StopReading;

   IMPORT RS232Int;   (* for RS232Int.Write and RS232Int.BusyRead *)

   FROM Strings IMPORT
      Length, Concat;

   FROM NumberConversion IMPORT
      StringToCard;

   IMPORT ASCII;


   VAR
      baudRate : CARDINAL;
      stopBits : CARDINAL;
      parityBit : BOOLEAN;
      evenParity : BOOLEAN;
      nbrOfBits : CARDINAL;
      OK : BOOLEAN;
      echo : (Off, Local, On);
      ch : CHAR;
      str : ARRAY [0..10] OF CHAR;
      n : CARDINAL;


   PROCEDURE Initialize;
      BEGIN
         Init (baudRate, stopBits, parityBit, evenParity, nbrOfBits, OK);
      END Initialize;


   PROCEDURE ClrScr;
   (* Clear the screen, and home the cursor *)
      BEGIN
         SETREG (AX, 0600H);   (* function 6 = scroll or clear window *)
         SETREG (BX, 0700H);   (* 7 = normal screen attribute *)
         SETREG (CX, 0000H);   (* top LH of screen *)
         SETREG (DX, 184FH);   (* bottom RH of screen *)
         SWI (10H);   (* call bios *)
         SETREG (AX, 0200h);   (* function 2 = position cursor *)
         SETREG (BX, 0000H);   (* page 0 *)
         SETREG (DX, 0000H);   (* home position *)
         SWI (10H);   (* call bios *)
      END ClrScr;


   PROCEDURE CommHelp;
   (* help menu for communications options *)
      BEGIN
         ClrScr;
         WriteString ("  C o m m u n i c a t i o n s   O p t i o n s");
         WriteLn;
         WriteString ("              H e l p   M e n u");
         WriteLn;   WriteLn;
         WriteString ("set Baud rate ................................ B");
         WriteLn;
         WriteString ("set Parity ................................... P");
         WriteLn;
         WriteString ("set Word length .............................. W");
         WriteLn;
         WriteString ("set Stop bits ................................ S");
         WriteLn;
         WriteString ("eXit ......................................... X");
         WriteLn;
      END CommHelp;


   PROCEDURE dispOpts;
   (* Display communications parameters for the user *)
      BEGIN
         WriteLn;
         WriteString ("Baud rate = ");   WriteCard (baudRate, 0);
         WriteString (";  ");
         IF parityBit THEN
            IF evenParity THEN
               WriteString ("Even ");
            ELSE
               WriteString ("Odd ");
            END;
         ELSE
            WriteString ("No ");
         END;
         WriteString ("parity;  ");
         WriteCard (nbrOfBits, 0);
         WriteString (" Data bits;  ");
         IF stopBits = 1 THEN
            WriteString ("One stop bit.");
         ELSE
            WriteString ("Two stop bits.");
         END;
         WriteLn;
      END dispOpts;


   PROCEDURE Options;
   (* set communications options *)

      VAR
         Quit : BOOLEAN;

      BEGIN
         ClrScr;
         Quit := FALSE;
         dispOpts;

         REPEAT
            WriteLn;   WriteLn;
            WriteString ("Set Communications Options [B, P, W, S, X, ?]: ");
            LOOP
               Terminal.Read (ch);
               CASE CAP (ch) OF
                  'B' : Baud;           EXIT;
               |  'P' : Parity;         EXIT;
               |  'W' : Word;           EXIT;
               |  'S' : Stops;          EXIT;
               |  '?' : CommHelp;       EXIT;
               |  'X' : Quit := TRUE;   EXIT;
               ELSE
                  (* ignore *)
               END;
            END;
            IF Quit THEN
               ClrScr;
            ELSE
               Initialize;
               dispOpts;
            END;
         UNTIL Quit;
      END Options;


   PROCEDURE Baud;
   (* Allow user to change the bit rate of the communications port *)
      BEGIN
         WriteString ("Baud Rate? [110 - 9600]: ");
         ReadString (str);
         IF Length (str) # 0 THEN
            StringToCard (str, n, OK);
            IF OK THEN
               CASE n OF
                   110, 150, 300, 600, 1200, 2400, 4800, 9600 : baudRate := n;
               ELSE
                  (* do nothing *)
               END;
            END;
         END;
      END Baud;


   PROCEDURE Word;
   (* Allow user to change the word length of the communications port *)
      BEGIN
         WriteString ("Word Length? [7, 8]: ");
         ReadString (str);
         IF Length (str) # 0 THEN
            StringToCard (str, n, OK);
            IF OK AND (n IN {7, 8}) THEN
               nbrOfBits := n;
            END;
         END;
      END Word;


   PROCEDURE Parity;
   (* Allow user to change the parity bit of the communications port *)
      BEGIN
         WriteString ("Parity? [None, Even, Odd]: ");
         ReadString (str);
         IF Length (str) # 0 THEN
            CASE CAP (str[0]) OF
               'N' : parityBit := FALSE;
            |  'E' : parityBit := TRUE;   evenParity := TRUE;
            |  'O' : parityBit := TRUE;   evenParity := FALSE;
            ELSE
               (* no action *)
            END;
         END;
      END Parity;


   PROCEDURE Stops;
   (* Allow user to change the number of stop bits *)
      BEGIN
         WriteString ("Stop Bits? [1, 2]: ");
         ReadString (str);
         IF Length (str) # 0 THEN
            StringToCard (str, n, OK);
            IF OK AND (n IN {1, 2}) THEN
               stopBits := n;
            END;
         END;
      END Stops;


   PROCEDURE Dir;

      VAR
         done, gotFN : BOOLEAN;
         path : ARRAY [0..60] OF CHAR;
         filename : ARRAY [0..20] OF CHAR;
         i, j, k : INTEGER;

      BEGIN
         filename := "";   (* in case no directory change *)
         WriteString ("Path? (*.*): ");
         ReadString (path);
         i := Length (path);
         IF i # 0 THEN
            gotFN := FALSE;
            WHILE (i >= 0) AND (path[i] # '\') DO
               IF path[i] = '.' THEN
                  gotFN := TRUE;
               END;
               DEC (i);
            END;
            IF gotFN THEN
               j := i + 1;
               k := 0;
               WHILE path[j] # 0C DO
                  filename[k] := path[j];
                  INC (k);       INC (j);
               END;
               filename[k] := 0C;
               IF (i = -1) OR (i = 0) AND (path[0] = '\')) THEN
                  INC (i);
               END;
               path[i] := 0C;
            END;
         END;
         IF Length (path) # 0 THEN
            DosCommand ("CHDIR", path, done);
         END;
         IF Length (filename) = 0 THEN
            filename := "*.*";
         END;
         Concat (filename, "/w", filename);
         ClrScr;
         DosCommand ("DIR", filename, done);
      END Dir;


   PROCEDURE ConnectHelp;
   (* provide help while in connect mode *)
      BEGIN
         ClrScr;
         WriteString ("LOCAL COMMANDS:");   WriteLn;
         WriteString ("^E = Echo mode");   WriteLn;
         WriteString ("^L = Local echo mode");   WriteLn;
         WriteString ("^T = Terminal mode (no echo)");   WriteLn;
         WriteString ("^X = eXit from connect");   WriteLn;
         WriteLn;  WriteLn;
      END ConnectHelp;


   PROCEDURE Connect;
   (* Terminal mode allows connection to host (possibly through MODEM) *)

      VAR
         Input : BOOLEAN;

      BEGIN
         ConnectHelp;
         REPEAT
            RS232Int.BusyRead (ch, Input);
            IF Input THEN
               IF ((ch >= 40C) AND (ch < 177C))
                OR (ch = ASCII.cr) OR (ch = ASCII.lf) OR (ch = ASCII.bs) THEN
                  Terminal.Write (ch);
               END;
               IF echo = On THEN
                  RS232Int.Write (ch);
               END;
            END;

            IF KeyPressed() THEN
               Terminal.Read (ch);
               IF ch = ASCII.enq THEN   (* Control-E *)
                  echo := On;
               ELSIF ch = ASCII.ff THEN   (* Control-L *)
                  echo := Local;
               ELSIF ch = ASCII.dc4 THEN   (* Control-T *)
                  echo := Off;
               ELSIF ((ch >= 40C) AND (ch < 177C))
                OR (ch = ASCII.EOL) OR (ch = ASCII.bs) THEN
                  IF ch = ASCII.EOL THEN
                     RS232Int.Write (ASCII.cr);
                     RS232Int.Write (ASCII.lf);
                  ELSE
                     RS232Int.Write (ch);
                  END;
                  IF (echo = On) OR (echo = Local) THEN
                     Terminal.Write (ch);
                  END;
               END;
            END;
         UNTIL ch = ASCII.can;   (* Control-X *)
      END Connect;


   PROCEDURE eXit (VAR q : BOOLEAN);
   (* Allow user to exit program after prompting for confirmation *)
      BEGIN
         WriteString ("Exit PCKermit? [Y/N]: ");
         Terminal.Read (ch);
         IF CAP (ch) = 'Y' THEN
            Terminal.Write ('Y');
            StopReading;   (* turn off the serial port *)
            q := TRUE;
         ELSE
            Terminal.Write ('N');
         END;
         WriteLn;
      END eXit;


   PROCEDURE MainHelp;
   (* help menu for  main program loop *)
      BEGIN
         ClrScr;
         WriteString ("  P C K e r m i t   H e l p   M e n u");    WriteLn;
         WriteLn;
         WriteString ("set communications Options ............. O");
         WriteLn;
         WriteString ("Connect to host ........................ C");
         WriteLn;
         WriteString ("Directory .............................. D");
         WriteLn;
         WriteString ("Send a file ............................ S");
         WriteLn;
         WriteString ("Receive a file ......................... R");
         WriteLn;
         WriteString ("eXit ................................... X");
         WriteLn;   WriteLn;
         WriteString ("To establish connection to Host:");   WriteLn;
         WriteString ("  -Use Connect Mode");   WriteLn;
         WriteString ("  -Dial Host (AT command set?)");   WriteLn;
         WriteString ("  -Log On to Host");   WriteLn;
         WriteString ("  -Issue Send (or Receive) command");   WriteLn;
         WriteString ("  -Return to main menu (^X)");   WriteLn;
         WriteString ("  -Issue Receive (or Send) command");   WriteLn;
         WriteLn;
      END MainHelp;


BEGIN   (* module initialization *)
   ClrScr;
   baudRate := 1200;
   stopBits := 1;
   parityBit := TRUE;
   evenParity := TRUE;
   nbrOfBits := 7;
   Initialize;
   StartReading;   (* turn on the serial port *)
   echo := Off;
END Shell.





<a name="00e6_0024"><a name="00e6_0024">
<a name="00e6_0025">
[LISTING SEVEN]
<a name="00e6_0025">

IMPLEMENTATION MODULE PAD;   (* Packet Assembler/Disassembler for Kermit *)

   FROM InOut IMPORT
      Write, WriteString, WriteInt, WriteHex, WriteLn;

   FROM Terminal IMPORT
      ReadString, Read, KeyPressed;

   FROM Strings IMPORT
      Length;

   FROM BitByteOps IMPORT
      ByteXor;

   FROM FileSystem IMPORT
      File;

   FROM Files IMPORT
      Status, FileType, Open, Create, CloseFile, Get, Put, DoWrite;

   FROM DataLink IMPORT
      FlushUART, SendPacket, ReceivePacket;

   IMPORT ASCII;


   CONST
      myMAXL = 94;
      myTIME = 10;
      myNPAD = 0;
      myPADC = 0C;
      myEOL  = 0C;
      myQCTL = '#';
      myQBIN = '&';
      myCHKT = '1';     (* one character checksum *)
      MAXtrys = 5;

   TYPE
      (* From Definition Module:
      PacketType = ARRAY [1..100] OF CHAR;
      *)
      PathnameType = ARRAY [0..40] OF CHAR;

   VAR
      yourMAXL : INTEGER;   (* maximum packet length -- up to 94 *)
      yourTIME : INTEGER;   (* time out -- seconds *)
      (* From Definition Module
      yourNPAD : INTEGER;   (* number of padding characters *)
      yourPADC : CHAR;   (* padding characters *)
      yourEOL  : CHAR;   (* End Of Line -- terminator *)
      *)
      yourQCTL : CHAR;   (* character for quoting controls '#' *)
      yourQBIN : CHAR;   (* character for quoting binary '&' *)
      yourCHKT : CHAR;   (* check type -- 1 = checksum, etc. *)
      sF, rF : File;   (* files being sent/received *)
      sFname, rFname : PathnameType;
      sP, rP : PacketType;   (* packets sent/received *)
      sSeq, rSeq : INTEGER;   (* sequence numbers *)
      PktNbr : INTEGER;   (* actual packet number -- no repeats up to 32,000 *)


   PROCEDURE Char (c : INTEGER) : CHAR;
   (* converts a number 0-94 into a printable character *)
      BEGIN
         RETURN (CHR (CARDINAL (ABS (c) + 32)));
      END Char;


   PROCEDURE UnChar (c : CHAR) : INTEGER;
   (* converts a character into its corresponding number *)
      BEGIN
         RETURN (ABS (INTEGER (ORD (c)) - 32));
      END UnChar;


   PROCEDURE Aborted() : BOOLEAN;

      VAR
         ch : CHAR;

      BEGIN
         IF KeyPressed() THEN
            Read (ch);
            IF ch = 033C THEN   (* Escape *)
               RETURN TRUE;
            END;
         END;
         RETURN FALSE;
      END Aborted;


   PROCEDURE TellError (Seq : INTEGER);
   (* Send error packet *)
      BEGIN
         sP[1] := Char (15);
         sP[2] := Char (Seq);
         sP[3] := 'E';   (* E-type packet *)
         sP[4] := 'R';   (* error message starts *)
         sP[5] := 'e';
         sP[6] := 'm';
         sP[7] := 'o';
         sP[8] := 't';
         sP[9] := 'e';
         sP[10] := ' ';
         sP[11] := 'A';
         sP[12] := 'b';
         sP[13] := 'o';
         sP[14] := 'r';
         sP[15] := 't';
         sP[16] := 0C;
         SendPacket (sP);
      END TellError;


   PROCEDURE ShowError (p : PacketType);
   (* Output contents of error packet to the screen *)

      VAR
         i : INTEGER;

      BEGIN
         FOR i := 4 TO UnChar (p[1]) DO
            Write (p[i]);
         END;
         WriteLn;
      END ShowError;


   PROCEDURE youInit (type : CHAR);
   (* I initialization YOU for Send and Receive *)
      BEGIN
         sP[1] := Char (11);   (* Length *)
         sP[2] := Char (0);   (* Sequence *)
         sP[3] := type;
         sP[4] := Char (myMAXL);
         sP[5] := Char (myTIME);
         sP[6] := Char (myNPAD);
         sP[7] := CHAR (ByteXor (myPADC, 100C));
         sP[8] := Char (ORD (myEOL));
         sP[9] := myQCTL;
         sP[10] := myQBIN;
         sP[11] := myCHKT;
         sP[12] := 0C;   (* terminator *)
         SendPacket (sP);
      END youInit;


   PROCEDURE myInit;
   (* YOU initialize ME for Send and Receive *)

      VAR
         len : INTEGER;

      BEGIN
         len := UnChar (rP[1]);
         IF len >= 4 THEN
            yourMAXL := UnChar (rP[4]);
         ELSE
            yourMAXL := 94;
         END;
         IF len >= 5 THEN
            yourTIME := UnChar (rP[5]);
         ELSE
            yourTIME := 10;
         END;
         IF len >= 6 THEN
            yourNPAD := UnChar (rP[6]);
         ELSE
            yourNPAD := 0;
         END;
         IF len >= 7 THEN
            yourPADC := CHAR (ByteXor (rP[7], 100C));
         ELSE
            yourPADC := 0C;
         END;
         IF len >= 8 THEN
            yourEOL := CHR (UnChar (rP[8]));
         ELSE
            yourEOL := 0C;
         END;
         IF len >= 9 THEN
            yourQCTL := rP[9];
         ELSE
            yourQCTL := 0C;
         END;
         IF len >= 10 THEN
            yourQBIN := rP[10];
         ELSE
            yourQBIN := 0C;
         END;
         IF len >= 11 THEN
            yourCHKT := rP[11];
            IF yourCHKT # myCHKT THEN
               yourCHKT := '1';
            END;
         ELSE
            yourCHKT := '1';
         END;
      END myInit;


   PROCEDURE SendInit;
      BEGIN
         youInit ('S');
      END SendInit;


   PROCEDURE SendFileName;

      VAR
         i, j : INTEGER;

      BEGIN
         (* send file name *)
         i := 4;   j := 0;
         WHILE sFname[j] # 0C DO
            sP[i] := sFname[j];
            INC (i);   INC (j);
         END;
         sP[1] := Char (j + 3);
         sP[2] := Char (sSeq);
         sP[3] := 'F';   (* filename packet *)
         sP[i] := 0C;
         SendPacket (sP);
      END SendFileName;


   PROCEDURE SendEOF;
      BEGIN
         sP[1] := Char (3);
         sP[2] := Char (sSeq);
         sP[3] := 'Z';   (* end of file *)
         sP[4] := 0C;
         SendPacket (sP);
      END SendEOF;


   PROCEDURE SendEOT;
      BEGIN
         sP[1] := Char (3);
         sP[2] := Char (sSeq);
         sP[3] := 'B';   (* break -- end of transmit *)
         sP[4] := 0C;
         SendPacket (sP);
      END SendEOT;


   PROCEDURE GetAck() : BOOLEAN;
   (* Look for acknowledgement -- retry on timeouts or NAKs *)

      VAR
         Type : CHAR;
         Seq : INTEGER;
         retrys : INTEGER;
         AckOK : BOOLEAN;

      BEGIN
         WriteString ("Sent Packet #");
         WriteInt (PktNbr, 5);
         WriteString ("  (ID: ");   WriteHex (sSeq, 4);
         WriteString ("h)");
         WriteLn;

         retrys := MAXtrys;
         LOOP
            IF Aborted() THEN
               TellError (sSeq);
               RETURN FALSE;
            END;
            IF (ReceivePacket (rP)) THEN
               Seq := UnChar (rP[2]);
               Type := rP[3];
               IF (Seq = sSeq) AND (Type = 'Y') THEN
                  AckOK := TRUE;
               ELSIF (Seq = (sSeq + 1) MOD 64) AND (Type = 'N') THEN
                  AckOK := TRUE;   (* NAK for (n + 1) taken as ACK for n *)
               ELSIF Type = 'E' THEN
                  ShowError (rP);
                  AckOK := FALSE;
                  retrys := 0;
               ELSE
                  AckOK := FALSE;
               END;
            ELSE
               AckOK := FALSE;
            END;
            IF AckOK OR (retrys = 0) THEN
               EXIT;
            ELSE
               WriteString ("Resending Packet #");
               WriteInt (PktNbr, 5);
               WriteString ("  (ID: ");   WriteHex (sSeq, 4);
               WriteString ("h)");
               WriteLn;
               DEC (retrys);
               FlushUART;
               SendPacket (sP);
            END;
         END;

         IF AckOK THEN
            INC (PktNbr);
            sSeq := (sSeq + 1) MOD 64;
            RETURN TRUE;
         ELSE
            RETURN FALSE;
         END;
      END GetAck;


   PROCEDURE GetInitAck() : BOOLEAN;
   (* configuration for remote station *)
      BEGIN
         IF GetAck() THEN
            myInit;
            RETURN TRUE;
         ELSE
            RETURN FALSE;
         END;
      END GetInitAck;


   PROCEDURE Send;
   (* Sends a file after prompting for filename *)

      VAR
         ch : CHAR;
         i : INTEGER;

      BEGIN
         WriteString ("Send: (filename?): ");
         ReadString (sFname);
         WriteLn;
         IF Length (sFname) = 0 THEN
            RETURN;
         END;
         IF Open (sF, sFname) # Done THEN
            WriteString ("No such file: ");   WriteString (sFname);
            WriteLn;
            RETURN;
         END;
         WriteString ("(<ESC> to abort file transfer.)");
         WriteLn;   WriteLn;
         FlushUART;
         sSeq := 0;   PktNbr := 0;
         SendInit;   (* my configuration information *)
         IF NOT GetInitAck() THEN     (* get your configuration information *)
            WriteString ("Excessive Errors...");   WriteLn;
            RETURN;
         END;

         SendFileName;
         IF NOT GetAck() THEN
            WriteString ("Excessive Errors...");   WriteLn;
            RETURN;
         END;

         (* send file *)
         i := 4;
         LOOP
            IF Aborted() THEN
               TellError (sSeq);
               RETURN;
            END;
            IF Get (sF, ch) = EOF THEN   (* send current packet & terminate *)
               sP[1] := Char (i - 1);
               sP[2] := Char (sSeq);
               sP[3] := 'D';   (* data packet *)
               sP[i] := 0C;   (* indicate end of packet *)
               SendPacket (sP);
               IF NOT GetAck() THEN
                  WriteString ("Excessive Errors...");   WriteLn;
                  RETURN;
               END;
               SendEOF;
               IF NOT GetAck() THEN
                  WriteString ("Excessive Errors...");   WriteLn;
                  RETURN;
               END;
               SendEOT;
               IF NOT GetAck() THEN
                  WriteString ("Excessive Errors...");   WriteLn;
                  RETURN;
               END;
               EXIT;
            END;

            IF i >= (yourMAXL - 4) THEN   (* send current packet *)
               sP[1] := Char (i - 1);
               sP[2] := Char (sSeq);
               sP[3] := 'D';
               sP[i] := 0C;
               SendPacket (sP);
               IF NOT GetAck() THEN
                  WriteString ("Excessive Errors...");   WriteLn;
                  RETURN;
               END;
               i := 4;
            END;

            (* add character to current packet -- update count *)
            IF ch > 177C THEN   (* must be quoted (QBIN) and altered *)
               (* toggle bit 7 to turn it off *)
               ch := CHAR (ByteXor (ch, 200C));
               sP[i] := myQBIN;   INC (i);
            END;
            IF (ch < 40C) OR (ch = 177C) THEN   (* quote (QCTL) and alter *)
               (* toggle bit 6 to turn it on *)
               ch := CHAR (ByteXor (ch, 100C));
               sP[i] := myQCTL;   INC (i);
            END;
            IF (ch = myQCTL) OR (ch = myQBIN) THEN   (* must send it quoted *)
               sP[i] := myQCTL;   INC (i);
            END;
            sP[i] := ch;   INC (i);
         END;   (* loop *)

         IF CloseFile (sF, Input) # Done THEN
            WriteString ("Problem closing source file...");   WriteLn;
         END;
      END Send;


   PROCEDURE ReceiveInit() : BOOLEAN;
   (* receive my initialization information from you *)

      VAR
         RecOK : BOOLEAN;
         errors : INTEGER;

      BEGIN
         errors := 0;
         LOOP
            IF Aborted() THEN
               TellError (rSeq);
               RETURN FALSE;
            END;
            RecOK := (ReceivePacket (rP)) AND (rP[3] = 'S');
            IF RecOK OR (errors = MAXtrys) THEN
               EXIT;
            ELSE
               INC (errors);
               SendNak;
            END;
         END;

         IF RecOK THEN
            myInit;
            RETURN TRUE;
         ELSE
            RETURN FALSE;
         END;
      END ReceiveInit;


   PROCEDURE SendInitAck;
   (* acknowledge your initialization of ME and send mine for YOU *)
      BEGIN
         WriteString ("Received Packet #");
         WriteInt (PktNbr, 5);
         WriteString ("  (ID: ");   WriteHex (rSeq, 4);
         WriteString ("h)");
         WriteLn;
         INC (PktNbr);
         rSeq := (rSeq + 1) MOD 64;
         youInit ('Y');
      END SendInitAck;


   PROCEDURE ValidFileChar (VAR ch : CHAR) : BOOLEAN;
   (* checks if character is one of 'A'..'Z', '0'..'9', makes upper case *)
      BEGIN
         ch := CAP (ch);
         RETURN ((ch >= 'A') AND (ch <= 'Z')) OR ((ch >= '0') AND (ch <= '9'));
      END ValidFileChar;


   TYPE
      HeaderType = (name, eot, fail);

   PROCEDURE ReceiveHeader() : HeaderType;
   (* receive the filename -- alter for local conditions, if necessary *)

      VAR
         i, j, k : INTEGER;
         RecOK : BOOLEAN;
         errors : INTEGER;

      BEGIN
         errors := 0;
         LOOP
            RecOK := ReceivePacket (rP) AND ((rP[3] = 'F') OR (rP[3] = 'B'));
            IF errors = MAXtrys THEN
               RETURN fail;
            ELSIF RecOK AND (rP[3] = 'F') THEN
               i := 4;   (* data starts here *)
               j := 0;   (* beginning of filename string *)
               WHILE (ValidFileChar (rP[i])) AND (j < 8) DO
                  rFname[j] := rP[i];
                  INC (i);   INC (j);
               END;
               REPEAT
                  INC (i);
               UNTIL (ValidFileChar (rP[i])) OR (rP[i] = 0C);
               rFname[j] := '.';   INC (j);
               k := 0;
               WHILE (ValidFileChar (rP[i])) AND (k < 3) DO
                  rFname[j + k] := rP[i];
                  INC (i);   INC (k);
               END;
               rFname[j + k] := 0C;
               WriteString ("Filename = ");   WriteString (rFname);   WriteLn;
               RETURN name;
            ELSIF RecOK AND (rP[3] = 'B') THEN
               RETURN eot;
            ELSE
               INC (errors);
               SendNak;
            END;
         END;
      END ReceiveHeader;


   PROCEDURE SendNak;
      BEGIN
         WriteString ("Requesting Repeat of Packet #");
         WriteInt (PktNbr, 5);
         WriteString ("  (ID: ");   WriteHex (rSeq, 4);
         WriteString ("h)");
         WriteLn;
         FlushUART;
         sP[1] := Char (3);   (* LEN *)
         sP[2] := Char (rSeq);
         sP[3] := 'N';   (* negative acknowledgement *)
         sP[4] := 0C;
         SendPacket (sP);
      END SendNak;


   PROCEDURE SendAck (Seq : INTEGER);
      BEGIN
         IF Seq # rSeq THEN
            WriteString ("Duplicate Packet      ");
         ELSE
            WriteString ("Received Packet #");   WriteInt (PktNbr, 5);
            rSeq := (rSeq + 1) MOD 64;
            INC (PktNbr);
         END;
         WriteString ("  (ID: ");   WriteHex (Seq, 4);
         WriteString ("h)");
         WriteLn;
         sP[1] := Char (3);
         sP[2] := Char (Seq);
         sP[3] := 'Y';   (* acknowledgement *)
         sP[4] := 0C;
         SendPacket (sP);
      END SendAck;


   PROCEDURE Receive;
   (* Receives a file  (or files) *)

      VAR
         ch, Type : CHAR;
         Seq : INTEGER;
         i : INTEGER;
         EOF, EOT, QBIN : BOOLEAN;
         errors : INTEGER;

      BEGIN
         WriteString ("Ready to receive file(s)...");   WriteLn;
         WriteString ("(<ESC> to abort file transfer.)");
         WriteLn;   WriteLn;
         FlushUART;
         rSeq := 0;   PktNbr := 0;
         IF NOT ReceiveInit() THEN   (* your configuration information *)
            WriteString ("Excessive Errors...");   WriteLn;
            RETURN;
         END;
         SendInitAck;       (* send my configuration information *)
         EOT := FALSE;
         WHILE NOT EOT DO
            IF Aborted() THEN
               TellError (rSeq);
               RETURN;
            END;
            CASE ReceiveHeader() OF
               eot  : EOT := TRUE;   EOF := TRUE;
            |  name : IF Create (rF, rFname) # Done THEN
                         WriteString ("Unable to open file: ");
                         WriteString (rFname);   WriteLn;
                         RETURN;
                      ELSE
                         PktNbr := 1;
                         EOF := FALSE;
                      END;
            |  fail : WriteString ("Excessive Errors...");   WriteLn;
                      RETURN;
            END;
            SendAck (rSeq);   (* acknowledge for name or eot *)
            WHILE NOT EOF DO
               IF Aborted() THEN
                  TellError (rSeq);
                  RETURN;
               END;
               IF ReceivePacket (rP) THEN
                  Seq := UnChar (rP[2]);
                  Type := rP[3];
                  IF Type = 'Z' THEN
                     EOF := TRUE;
                     IF CloseFile (rF, Output) # Done THEN
                        WriteString ("Error closing file: ");
                        WriteString (rFname);   WriteLn;
                        RETURN;
                     END;
                     SendAck (rSeq);
                  ELSIF Type = 'E' THEN
                     ShowError (rP);
                     RETURN;
                  ELSIF (Type = 'D') AND ((Seq + 1) MOD 64 = rSeq) THEN
                  (* discard duplicate packet, and Ack anyway *)
                     SendAck (Seq);
                  ELSIF (Type = 'D') AND (Seq = rSeq) THEN
                     (* put packet into file buffer *)
                     i := 4;   (* first data in packet *)
                     WHILE rP[i] # 0C DO
                        ch := rP[i];   INC (i);
                        IF ch = yourQBIN THEN
                           ch := rP[i];   INC (i);
                           QBIN := TRUE;
                        ELSE
                           QBIN := FALSE;
                        END;
                        IF ch = yourQCTL THEN
                           ch := rP[i];   INC (i);
                           IF (ch # yourQCTL) AND (ch # yourQBIN) THEN
                              ch := CHAR (ByteXor (ch, 100C));
                           END;
                        END;
                        IF QBIN THEN
                           ch := CHAR (ByteXor (ch, 200C));
                        END;
                        Put (ch);
                     END;

                     (* write file buffer to disk *)
                     IF DoWrite (rF) # Done THEN
                        WriteString ("Error writing to file: ");
                        WriteString (rFname);   WriteLn;
                        RETURN;
                     END;
                     errors := 0;
                     SendAck (rSeq);
                  ELSE
                     INC (errors);
                     IF errors = MAXtrys THEN
                        WriteString ("Excessive errors...");   WriteLn;
                        RETURN;
                     ELSE
                        SendNak;
                     END;
                  END;
               ELSE
                  INC (errors);
                  IF errors = MAXtrys THEN
                     WriteString ("Excessive errors...");   WriteLn;
                     RETURN;
                  ELSE
                     SendNak;
                  END;
               END;
            END;
         END;
      END Receive;


BEGIN   (* module initialization *)
   yourEOL := ASCII.cr;
   yourNPAD := 0;
   yourPADC := 0C;
END PAD.





<a name="00e6_0026"><a name="00e6_0026">
<a name="00e6_0027">
[LISTING EIGHT]
<a name="00e6_0027">


IMPLEMENTATION MODULE Files;   (* File I/O for Kermit *)

   FROM FileSystem IMPORT
      File, Response, Delete, Lookup, Close, ReadNBytes, WriteNBytes;

   FROM InOut IMPORT
      Read, WriteString, WriteLn, Write;

   FROM SYSTEM IMPORT
      ADR, SIZE;


   TYPE
      buffer = ARRAY [1..512] OF CHAR;

   VAR
      inBuf, outBuf : buffer;
      inP, outP : CARDINAL;   (* buffer pointers *)
      read, written : CARDINAL;   (* number of bytes read or written *)
                                  (* by ReadNBytes or WriteNBytes    *)


   PROCEDURE Open (VAR f : File; name : ARRAY OF CHAR) : Status;
   (* opens an existing file for reading, returns status *)
      BEGIN
         Lookup (f, name, FALSE);
         IF f.res = done THEN
            inP := 0;   read := 0;
            RETURN Done;
         ELSE
            RETURN Error;
         END;
      END Open;


   PROCEDURE Create (VAR f : File; name : ARRAY OF CHAR) : Status;
   (* creates a new file for writing, returns status *)

      VAR
         ch : CHAR;

      BEGIN
         Lookup (f, name, FALSE);   (* check to see if file exists *)
         IF f.res = done THEN
            Close (f);
            WriteString ("File exists!  Overwrite? (Y/N): ");
            Read (ch);   Write (ch);   WriteLn;
            IF CAP (ch) = 'Y' THEN
               Delete (name, f);
               Close (f);
            ELSE
               RETURN Error;
            END;
         END;
         Lookup (f, name, TRUE);
         IF f.res = done THEN
            outP := 0;
            RETURN Done;
         ELSE
            RETURN Error;
         END;
      END Create;


   PROCEDURE CloseFile (VAR f : File; Which : FileType) : Status;
   (* closes a file after reading or writing *)
      BEGIN
         written := outP;
         IF (Which = Output) AND (outP > 0) THEN
            WriteNBytes (f, ADR (outBuf), outP, written);
         END;
         Close (f);
         IF (written = outP) AND (f.res = done) THEN
            RETURN Done;
         ELSE
            RETURN Error;
         END;
      END CloseFile;


   PROCEDURE Get (VAR f : File; VAR ch : CHAR) : Status;
   (* Reads one character from the file, returns status *)
      BEGIN
         IF inP = read THEN
            ReadNBytes (f, ADR (inBuf), SIZE (inBuf), read);
            inP := 0;
         END;
         IF read = 0 THEN
            RETURN EOF;
         ELSE
            INC (inP);
            ch := inBuf[inP];
            RETURN Done;
         END;
      END Get;


   PROCEDURE Put (ch : CHAR);
   (* Writes one character to the file buffer *)
      BEGIN
         INC (outP);
         outBuf[outP] := ch;
      END Put;


   PROCEDURE DoWrite (VAR f : File) : Status;
   (* Writes buffer to disk only if nearly full *)
      BEGIN
         IF outP < 400 THEN   (* still room in buffer *)
            RETURN Done;
         ELSE
            WriteNBytes (f, ADR (outBuf), outP, written);
            IF (written = outP) AND (f.res = done) THEN
               outP := 0;
               RETURN Done;
            ELSE
               RETURN Error;
            END;
         END;
      END DoWrite;

END Files.





<a name="00e6_0028"><a name="00e6_0028">
<a name="00e6_0029">
[LISTING NINE]
<a name="00e6_0029">


IMPLEMENTATION MODULE DataLink;   (* Sends and Receives Packets for PCKermit *)

   FROM InOut IMPORT
      WriteString, WriteLn;

   FROM Delay IMPORT
      Delay;   (* delay is in milliseconds *)

   FROM BitByteOps IMPORT
      ByteAnd;

   IMPORT RS232Int;   (* for RS232Int.BusyRead, RS232Int.Write *)

   FROM PAD IMPORT
      PacketType, yourNPAD, yourPADC, yourEOL;

   IMPORT ASCII;


   CONST
      MAXtime = 10000;
      MAXsohtrys = 100;

   VAR
      ch : CHAR;
      GotChar : BOOLEAN;


   PROCEDURE Char (c : INTEGER) : CHAR;
   (* converts a number 0-95 into a printable character *)
      BEGIN
         RETURN (CHR (CARDINAL (ABS (c) + 32)));
      END Char;


   PROCEDURE UnChar (c : CHAR) : INTEGER;
   (* converts a character into its corresponding number *)
      BEGIN
         RETURN (ABS (INTEGER (ORD (c)) - 32));
      END UnChar;


   PROCEDURE FlushUART;
   (* ensure no characters left in UART holding registers *)
      BEGIN
         Delay (500);
         REPEAT
            RS232Int.BusyRead (ch, GotChar);
         UNTIL NOT GotChar;
      END FlushUART;


   PROCEDURE SendPacket (s : PacketType);
   (* Adds SOH and CheckSum to packet *)

      VAR
         i : INTEGER;
         checksum : INTEGER;

      BEGIN
         Delay (10);   (* give host a chance to catch its breath *)
         FOR i := 1 TO yourNPAD DO
            RS232Int.Write (yourPADC);
         END;
         RS232Int.Write (ASCII.soh);
         i := 1;
         checksum := 0;
         WHILE s[i] # 0C DO
            INC (checksum, ORD (s[i]));
            RS232Int.Write (s[i]);
            INC (i);
         END;
         checksum := checksum + (INTEGER (BITSET (checksum) * {7, 6}) DIV 64);
         checksum := INTEGER (BITSET (checksum) * {5, 4, 3, 2, 1, 0});
         RS232Int.Write (Char (checksum));
         IF yourEOL # 0C THEN
            RS232Int.Write (yourEOL);
         END;
      END SendPacket;


   PROCEDURE ReceivePacket (VAR r : PacketType) : BOOLEAN;
   (* strips SOH and checksum -- return FALSE if timed out or bad checksum *)

      VAR
         sohtrys, time : INTEGER;
         i, len : INTEGER;
         ch : CHAR;
         checksum : INTEGER;
         mycheck, yourcheck : CHAR;

      BEGIN
         sohtrys := MAXsohtrys;
         REPEAT
            time := MAXtime;
            REPEAT
               DEC (time);
               RS232Int.BusyRead (ch, GotChar);
            UNTIL GotChar OR (time = 0);
            ch := CHAR (ByteAnd (ch, 177C));   (* mask off MSB *)
            (* skip over up to MAXsohtrys padding characters, *)
            (* but allow only MAXsohtrys/10 timeouts          *)
            IF GotChar THEN
               DEC (sohtrys);
            ELSE
               DEC (sohtrys, 10);
            END;
         UNTIL (ch = ASCII.soh) OR (sohtrys <= 0);

         IF ch = ASCII.soh THEN
            (* receive rest of packet *)
            time := MAXtime;
            REPEAT
               DEC (time);
               RS232Int.BusyRead (ch, GotChar);
            UNTIL GotChar OR (time = 0);
            ch := CHAR (ByteAnd (ch, 177C));
            len := UnChar (ch);
            r[1] := ch;
            checksum := ORD (ch);
            i := 2;   (* on to second character in packet -- after LEN *)
            REPEAT
               time := MAXtime;
               REPEAT
                  DEC (time);
                  RS232Int.BusyRead (ch, GotChar);
               UNTIL GotChar OR (time = 0);
               ch := CHAR (ByteAnd (ch, 177C));
               r[i] := ch;   INC (i);
               INC (checksum, (ORD (ch)));
            UNTIL (i > len);
            time := MAXtime;
            REPEAT
               DEC (time);
               RS232Int.BusyRead (ch, GotChar);
            UNTIL GotChar OR (time = 0);   (* get checksum character *)
            ch := CHAR (ByteAnd (ch, 177C));
            yourcheck := ch;
            r[i] := 0C;
            checksum := checksum +
                            (INTEGER (BITSET (checksum) * {7, 6}) DIV 64);
            checksum := INTEGER (BITSET (checksum) *  {5, 4, 3, 2, 1, 0});
            mycheck := Char (checksum);
            IF mycheck = yourcheck THEN   (* checksum OK *)
               RETURN TRUE;
            ELSE   (* ERROR!!! *)
               WriteString ("Bad Checksum");   WriteLn;
               RETURN FALSE;
            END;
         ELSE
            WriteString ("No SOH");   WriteLn;
            RETURN FALSE;
         END;
      END ReceivePacket;

END DataLink.













Copyright © 1989, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.