{$A+}{$B-}{$D+}{$G+}{$R-}{$S-}{$V-}
(*
  $A+: Align on word boundaries (for 80x86 processors
  $B-: short circuit boolean evaluation
  $G+: enable 80286 code optimization
  $L : local symbols switch
  $R+- only adds time when an index is used in array or a string
  $S+- checks stack whenever a procedure is called or a dynamic variable
       is created.
  $V+: Controls type-checking on strings passed as variable parameters
 
*)
(*
   PROTOCOL.PAS - protocol unit for NBBS BBS v1.00a
   (c)1989,1990,1993 Eric J. Givler, All Rights Reserved.

   History:

   Internal Functions and Procedures in this unit include:
   function eltime      - elapsed time calculations of transfers.
   function leap        - return true if year is a leap year
   function octal       - return octal string of a longint
   function since70     - Calculate seconds since 01/01/70
   function sendxmodem  - send xmodem/checksum
   function sendxmodemCRC guess?
   function send1kxmodem- send Xmodem-1K
   function sendymodem  - send true Ymodem (has header info)
   function sendascii   - not done
   function recvascii   - not done
   function recvxmodem  - Receive Xmodem/Checksum

   Dispatcher functions (CALLABLE)
   FUNCTION UpLoad(fname: string; using:protocols): boolean;
   FUNCTION DownLoad(fname: string; using:protocols): boolean;


   FOR A USER WHO DOESN'T HAVE MNP:
   var valid_protocol_set : set of protocol;

   valid_protocol_set := protocol_set - MNP_set;

   YOU CAN THEN STEP THROUGH THE SET, PRESENT THE USER WITH WHAT PROTOCOLS
   ARE AVAILABLE, AND THEN USE THE UNIT TO INITIATE THE TRANSFER.  LIKE:

   var p: protocol;
   p := integer(0);
   repeat
     writeln('How about using ', protocol_name[p]);
     p := succ(p);
   until (p = External);
*)
UNIT PROTOCOL;

INTERFACE

type protocols = (ASCII, XmodemChkSum, XmodemCRC, Xmodem1K, Ymodem,
                 MegaLink,YmodemG);

const protocol_name: array[protocols] of string[12] =
                ('ASCII','XmodemChkSum','XmodemCRC','Xmodem1K',
                 'Ymodem','MegaLink','YmodemG');

      protocol_set : set of protocols = [ASCII..YmodemG];
      batch_set    : set of protocols = [Ymodem,YmodemG,MegaLink];
      MNP_set      : set of protocols = [YmodemG];

var errorcode : byte;
{
  0 = No Error, Success
  1 = User/Remote Aborted Transfer
  2 = Local Abort
  3 = Carrier Loss
  4 = Bad CRC
  5 = No ACK on EOT
  6 = File already exists?
  7 = File NOT found
}
   cps : real;  { result of last transfer - Characters Per Second }


(* protocol dispatchers *)
function Upload(fname: string; using : protocols): boolean;
function Download(fname: string; using : protocols): boolean;

(* ------------------------- IMPLEMENTATION ---------------------------- *)
IMPLEMENTATION

USES DOS,
     crt,     { Turbo Pascal CRT routines    }
     crcs,    { CRC calculation routines     }
     fos;     { Fossil communication library }

CONST NUL  = 00;
      SOH  = #$01;            { Start Of Header (128)   }
      STX  = #$02;            { Start Of Header (1024)  }
      EOT  = #$04;            { End of Transmission     }
      ACK  = #$06;            { Acknowledge (positive)  }
      DLE  = #$10;            { Data Link Escape        }
      NAK  = #$15;            { Negative Acknowledge    }
      SYN  = #$16;            { Synchronous idle        }
      XON  = #$11;            { Transmit On (DC1)       }
      XOFF = #$13;            { Transmit Off (DC3)      }
      CAN  = #$18;            { Cancel                  }
      CPMEOF = #$1A;          { End Of File (padding)^Z }

      C   = #$43;
      TAB = 09;
      LF  = #$0A;
      CR  = #$0D;
      Space = ' ';

      lastbyte = 127;
      errormax = 5;
      retrymax = 10;             { 10 retries }

type  blocktype = array[0..127] of byte;

VAR   sector : blocktype;        { array[0..lastbyte] of byte; }
      systicks  : longint absolute $40:$6c;
      tickstart : real;


function eltime(lesser,greater:real):real;
begin
   if lesser <= greater then
        eltime := greater - lesser
   else eltime := (86400.0 - lesser) + greater;
end; (* eltime (elapsed time) for reals *)


FUNCTION SENDXMODEM(var f : file): boolean;
{ currently no abort local or remote allowed here!! }
var j,                            { for local loops }
    result,
    checksum,
    blocknum,
    ch       : integer;
    lc       : char;              { possible local abort }
    counter  : byte;
    temp     : string[5];
begin
  sendxmodem := false;
  blocknum := 1;
  str((filesize(f) div 128):5,temp);
  writeln('File open:' + temp + ' records.');
  repeat
     counter := 0;
     fillchar(sector,sizeof(sector),CPMEOF);
     blockread(f,sector,sizeof(sector),result);
     repeat
       write(cr,'Sending block: ',blocknum);
       FOS.Send(SOH);                             { Start of Header  }
       FOS.Send(CHR(blocknum));                   { Packet Number    }
       FOS.Send(CHR(-blocknum-1));                { One's complement }
       CHECKSUM := 0;
       FOS.Sendblk(seg(sector[0]),ofs(sector[0]),128);
       for j:= 0 to lastbyte do CHECKSUM:=(CHECKSUM+sector[j]) mod 256;
       send(chr(CHECKSUM));
       purgeline;
       inc(counter);
       ch := readline(10);
       if keypressed then lc := readkey;
     until (ch in [Ord(ACK),Ord(CAN)]) or (counter = retrymax) or (NOT carrier);
     if (ch = Ord(CAN)) or (lc = #27) then
     begin
        errorcode := 1;
        exit;
     end;        
     inc(blocknum);
  until eof(f) or (counter = retrymax) or (not FOS.carrier);
  if counter = retrymax then
  begin
     Writeln(cr,lf,'No ACK on sector');
     errorcode := 1;
  end
  else
  begin
     counter := 0;
     repeat
        send(EOT);
        inc(counter);
     until (readline(10)=ord(ACK)) or (counter=retrymax) or (not carrier);
     if counter = retrymax then
     begin
        WriteLn(cr,lf,'No ACK on EOT');
        errorcode := 1;
     end
     else
     begin
        WriteLn(cr,lf,'Transfer complete');
        errorcode := 0;
        sendxmodem := TRUE;
     end;
  end;
end;


FUNCTION SendXmodemCRC( var f : file ) : boolean;
VAR  temp    : string[5];
     counter,
     result  : word;
     j,k,blocknum: integer;
BEGIN
   blocknum := 1;
   str((filesize(f) div 128):5,temp);
   writeln('File open:' + temp + ' records.');
   REPEAT
      counter := 0;
      FillChar(sector,SizeOF(sector),CPMEOF);
      {$I-} Blockread(f,sector,sizeof(sector),result); {$I+}
      if IOResult <> 0 THEN
      begin
          WriteLn('Error Reading File: CANCELLED');
          Send(CAN); 
          Send(CAN);
          Exit;
      end;
      REPEAT
         Write(cr,'Sending block# ',blocknum);
         Send(SOH);
         Send(CHR(blocknum));
         Send(CHR(-blocknum-1));
         SendBlk( seg(sector[0]), ofs(sector[0]), 128);
         crc := 0;
         Crca(sector,SizeOf(sector),crc);
         Send(CHR(Hi(crc)));
         Send(CHR(Lo(crc)));           
         PurgeLine;
         inc(counter);
      UNTIL (readline(10) = Ord(ACK)) OR (counter = retrymax);
      Inc(blocknum);
   UNTIL EOF(f) OR (counter = retrymax) OR (NOT Carrier);
   if counter = retrymax THEN
       writeln(cr,lf,'No ACK on sector')
   else 
   begin
      counter := 0;
      repeat
         Send(EOT);
         Inc(counter);
      until (readline(10)=Ord(ACK)) or (counter=retrymax);
      if counter = retrymax then
          writeln(cr,lf,'No ACK on EOT')
      else WriteLn(cr,lf,'Transfer complete');
   end;
END;


FUNCTION SendAscii(fname:string):boolean;
{ establish any flow control before calling this function }
var thefile : TEXT;
    inch,ch,lc : char;
begin
   SendAscii := FALSE;
   ch := ' '; lc := ' ';
   assign(thefile,fname);
   {$I-} Reset(thefile); {$I+}
   if ioresult <> 0 then begin
      errorcode := 7; { file not found }
      exit;
   end;
   repeat
     read(thefile, inch);
     send(inch);
     if serialchar then ch := receive;
     if keypressed then lc := readkey;
     {
     if ch = chr(ord(xoff))) then
        repeat 
           if serialchar then ch := receive;
        until ch = chr(ord(xon));
     }
   until eof(thefile) OR (not carrier) or (ch = ^X) or (lc = #27);
   send(^Z);
   close(thefile);
   SendAscii := TRUE;
   errorcode := 0;
   if not carrier then begin
      errorcode := 3;  SendAscii := FALSE;
   end else if ch = ^X then begin
      errorcode := 1;  SendAscii := FALSE;
   end else if lc = #27 then begin
      errorcode := 2;  SendAscii := FALSE;
   end;
end;


function octal( t : LongInt) : String;
{ FUNCTION  octal   - Returns OCTAL string of a LongInt (seconds) }
var quotient, remainder : longint;
    code : integer;
    os : string;
    ch : string[1];
begin
    os := '';
    ch := ' ';
    quotient := t;
    while (quotient <> 0) do begin
       quotient := quotient DIV 8;
       remainder := t MOD 8;
       t := quotient;
       str(remainder,ch);
       os := ch + os;
   end;
   octal := os;
end;


function leap( yr : integer) : BOOLEAN;
{ FUNCTION  leap    - Returns TRUE if yr is a leapyear. }
begin
   if (((yr mod 4 = 0) and (yr mod 100 <> 0)) or (yr mod 400 = 0)) then 
      leap := TRUE 
   else leap := FALSE;
end;


function since70(dt : datetime) : longint;
{ FUNCTION  since70 - Calculates seconds since 01/01/70 for LAST UPDATE }
const month : array[1..12] of integer = (31,28,31,30,31,30,31,31,30,31,30,31);
var i, leapyrs : integer;
    secs, thisyear : longint;
begin
   leapyrs := 0;
   for i := 1970 to (dt.year - 1) do if leap(i) then inc(leapyrs);
   secs := (dt.year - 1970)*86400*365 + leapyrs*86400;
   thisyear := (longint(dt.hour) * 60 * 60) + (dt.min * 60) + (dt.sec) +
               ((dt.day - 1) * 86400);
   for i := 1 to (dt.month-1) do thisyear := thisyear + (month[i]*86400);
   if leap(dt.year) and (dt.month > 2) then thisyear := thisyear + 86400;
   since70 := secs + thisyear;
end;


{============================== SendYmodem =============================}
FUNCTION  SENDYMODEM( filename : string; var f : file ) : boolean;
CONST NULL = $0;
VAR block : array[0..1023] of byte; (* byte *)
     temp : string[5];
     j,i  : integer;
    str1  : string;
   ftime  : longint;
   tcrc   : word;
   dt : datetime;
   blocknum,
   counter,
   result : integer;
BEGIN

     (* Build Ymodem header block - block 0 *)
     FillChar(sector,SizeOf(sector),NULL); { chr(0) }
     for j := 0 to length(filename)-1 DO sector[j] := Ord(filename[j+1]);
     inc(j);
     str(FileSize(f),str1);
     for i := 1 to length(str1) DO sector[j+i] := Ord(str1[i]);
     j := j + i + 1;
     sector[j] := $20;
     GetFTime(f,ftime);
     UnPackTime(ftime,dt);
     str1 := Octal(Since70(dt));
     For i := 1 to length(str1) do sector[j+i] := Ord(str1[i]);
     sector[j+i+1] := $20;

     (* Send header packet *)
     REPEAT
        Send(SOH);
        Send(#0);
        Send(#$FF);
        SendBlk(seg(sector[0]),ofs(sector[0]),128);
        crc := 0;
        crca(Sector,SizeOf(sector),crc);
        Send(CHR(Hi(crc)));
        Send(CHR(Lo(crc)));
        PurgeLine;
     UNTIL (readline(10) = Ord(ACK));

     blocknum := 1;
     str((filesize(f) DIV 1024):5,temp);
     WriteLn('File open:' + temp + ' records.');
     REPEAT
        counter := 0;
        FillChar(block,SizeOf(block),CPMEOF);
        {$I-} blockread(f,block,SizeOf(block),result); {$I+}
        if IOResult <> 0 then
        begin
           WriteLn('Error Reading File: CANCELLED');
           FOS.Send(CAN);
           FOS.Send(CAN);
           Exit;
        end;
        REPEAT
           Write(cr,'Sending block: ',blocknum);
           Send(STX);
           Send(CHR(blocknum));
           Send(CHR(-blocknum-1));
           SendBlk(seg(block[0]),ofs(block[0]),1024);
           crc := 0;
           Crca(block,sizeof(block),crc);
           Send(CHR(Hi(crc)));
           Send(CHR(Lo(crc)));
           PurgeLine;
           Inc(counter);
        UNTIL (readline(10) = Ord(ACK)) OR (counter = retrymax);
        inc(blocknum);
     UNTIL EOF(f) OR (counter = retrymax) OR (NOT Carrier);

     IF counter = retrymax THEN
         Writeln(CR,LF,'No ACK on sector')
     ELSE
     BEGIN
         counter := 0;
         REPEAT
           Send(EOT);
           Inc(counter);
         UNTIL (readline(10) = Ord(ACK)) or (counter=retrymax);
         IF counter = retrymax THEN
            WriteLn(CR,LF,'No ACK on EOT')
         ELSE WriteLn(CR,LF,'Transfer complete');
     END;

     (*  Send a null header block to signify end of transfer! *)
     counter := 0;
     REPEAT
        FillChar(sector,SizeOf(sector),CHR(0));  { NULL := CHR(0) }
        Send(SOH);
        Send(#$00);
        Send(#$FF);
        SendBlk(seg(sector[0]),ofs(sector[0]),128);
        crc := 0;
        crca(Sector, SizeOf(sector), crc);
        Send(CHR(Hi(crc)));
        Send(CHR(Lo(crc)));
        inc(counter);
     UNTIL (Readline(10) = Ord(ACK)) or (counter = retrymax);
END;


(*  
    PROCEDURE PackDateAndTime(var pd : date; dt : DateTime);
    { Returns the number of seconds since 00:00:00 01/01/1970 }
    CONST TDays : array[boolean,0..12] of word =
           ((0,31,59,90,120,151,181,212,243,273,304,334,365),
           (0,31,60,91,121,152,182,213,244,274,305,335,366));
          diff  = 347155200;
    VAR total,
        temp   : date;
        lyr    : boolean;
    BEGIN
       lyr := (((dt.year mod 4 = 0) and (dt.year mod 100 <>0))
              or (dt.year mod 400 = 0));
       dec(dt.year,1981);
       total := date(dt.sec) + (dt.min * 60) + (date(dt.hour) * 3600);
       temp := date(dt.year) * word(365) + (dt.year div 4);
       inc(temp,TDays[lyr][dt.month-1]);
       inc(temp,dt.day-1);
       pd := total + (temp * 86400) + diff;
    END;  {PackDateAndTime}

    crc := 0;
    crca(block, SizeOf(block), crc);
    Send(CHR(Hi(crc)));
    Send(CHR(Lo(crc)));
    BlockCRC(Seg(block),Ofs(block),1023);
    Send(CHR(Hi(crc_reg_hi)));
    Send(CHR(Lo(crc_reg_hi)));

    BlockCRC(Seg(sector[0]),ofs(sector[0]),127);
    Send(CHR(Hi(crc_reg_hi)));
    Send(CHR(Lo(crc_reg_hi)));

           {FOR j := 0 TO 1023 do begin
               Send(block[j]);
               updcrc(tcrc,block[j]);
           end;
           }
*)

FUNCTION SEND1KXMODEM( var f : file ) : boolean;
VAR block  : array[0..1023] of byte;
    temp   : string[5];
    result : word;
    counter,
    blocknum,
    j     : integer;
BEGIN
   blocknum := 1;
   str((filesize(f) DIV 1024):5,temp);
   WriteLn(#13+#10'File open:' + temp + ' records.');
   repeat
     counter := 0;
     FillChar(block,SizeOf(block),CPMEOF);
     {$I-} blockread(f,block,SizeOf(block),result); {$I+}
     if IOResult <> 0 then
     begin
        WriteLn('Error Reading File: CANCELLED');
        Send(CAN);
        Send(CAN);
        Exit;
     end;
     repeat
        Write(cr,'Sending block: ',blocknum);
        Send(STX);                              {  Send(SOH);     }
        Send(CHR(blocknum));
        Send(CHR(-blocknum-1));                 { (-blocknum-1)); }
        For j := 0 to 1023 do Send(CHR(block[j]));
        crc := 0;
        crca(block,1024,crc);
        Send(CHR(Hi(crc)));
        Send(CHR(Lo(crc)));
        PurgeLine;
        Inc(counter);
        { ch := readline(10);  write('ch:',ch,#7); }
     until (readline(10) =Ord(ACK)) OR (counter = retrymax);
     WRITE(COUNTER);
     inc(blocknum);
   until EOF(f) OR (counter = retrymax) OR (NOT FOS.Carrier);
   IF counter = retrymax THEN
      Writeln(cr,lf,'No ACK on sector')
   else
   begin
      counter := 0;
      repeat
         Send(EOT);
         Inc(counter);
      until (readline(10)=Ord(ACK)) or (counter=retrymax);
      IF counter = retrymax THEN
         WriteLn(cr,lf,'No ACK on EOT')
      ELSE WriteLn(cr,lf,'Transfer complete');
   end;
end;


{====================================================================
 UPLOAD DISPATCHER
 ====================================================================}
FUNCTION UPLOAD(fname: string; using:protocols): boolean;
VAR result   : boolean;
    workfile : file;
    sizeoffile : longint;
    elapsed  : word;
BEGIN
    result := FALSE;
    assign(workfile,fname);
    {$I-} reset(workfile,1); {$I+}
    if ioresult <> 0 then
        errorcode := 7
    else
    begin
        tickstart := systicks / 18.23;
        sizeoffile:= filesize(workfile);
        case using of
           {Ascii       : result := SendAscii(fname);}
           XmodemChkSum : result := SendXmodem( workfile );
           XmodemCRC    : result := SendXmodemCRC( workfile );
           Xmodem1K     : result := Send1KXmodem( workfile );
           Ymodem       : result := SendYmodem(fname, workfile );
        else
           write('Protocol currently unavailable!',#7);
        end;
        close(workfile);
        Upload  := result;
        elapsed := trunc(Eltime( tickstart, (systicks/18.23) ));
        writeln('Elapsed Seconds: ', elapsed );
        cps     := sizeoffile / elapsed;
        writeln('Cps: ', cps:7:2)
    end;
END;


{==========================================================================
  Receive protocols and dispatcher follow
===========================================================================}
FUNCTION recvascii(fname:string) : boolean;
var  lc,rc:char;
     textfile : TEXT;
begin
  recvascii := FALSE;
  lc := ' ';
  rc := ' ';
  assign(textfile,fname);
  {$I-} Reset(textfile); {$I+}
  if (IOResult = 0) then begin
     close(textfile);
     errorcode := 6;
     exit;
  end;
  rewrite(textfile);
  SendText('Ends on Ctrl-Z, Abort with Ctrl-X');
  Writeln('Type ^X to exit ASCII receive');
  repeat
    If SerialChar THEN rc := Receive;
    If Keypressed THEN lc := ReadKey;
    Write(textfile,rc);
  until (rc = ^Z) OR (rc = ^X) OR (lc = #27) OR (NOT Carrier);
  close(textfile);
  if rc = ^Z then begin
     errorcode := 0;
     recvascii := TRUE;
     exit;
  end;
  if rc = ^X then errorcode := 1
  else if lc = #27 then errorcode := 2
  else if NOT carrier then errorcode := 3;
  erase(textfile);
end;


FUNCTION RecvXmodem(fname:string) : boolean;
VAR j,
    firstchar,
    sectornum,
    sectorcurrent,
    sectorcomp,
    errors,
    checksum  : integer;
    errorflag : boolean;
    c         : char;
    workfile  : file;

begin
   RecvXmodem := FALSE;
   assign(workfile,fname);
   rewrite(workfile);
   if Ioresult <> 0 then begin
      errorcode := 6;
      exit;
   end;
   sectornum := 0;
   errors := 0;
   send(NAK);
   send(NAK);                       (* send ready characters *)
   repeat
     errorflag := false;
     repeat
       firstchar := readline(20);
     until ((firstchar IN [Ord(SOH),Ord(EOT)]) OR
           (firstchar = timeout)) OR (Not Carrier);
     if NOT Carrier THEN begin
        errorcode := 3;
        exit;
     end;
     IF firstchar = timeout THEN Writeln(cr,lf,'Error - No starting SOH');
     IF firstchar = Ord(SOH) THEN BEGIN
        sectorcurrent := Readline(1);      {real sector number}
        sectorcomp    := Readline(1);      {+ inverse of above}
        IF (sectorcurrent+sectorcomp) = 255 THEN BEGIN {< becomes this #}
           IF (sectorcurrent=sectornum+1) THEN BEGIN
              checksum := 0;
              ReadBlk(seg(sector[0]),ofs(sector[0]),128);
              for j:= 0 to lastbyte do 
                  checksum := (checksum+sector[j]) mod 256;
              IF checksum = Readline(1) THEN BEGIN
                 blockwrite(WorkFile,sector,1);
                 errors := 0;
                 sectornum := sectorcurrent;
                 write(cr,'Received sector ',sectorcurrent);
                 send(ACK)
              END ELSE BEGIN
                 writeln(cr,lf,'Checksum error');
                 errorflag := true
              END
           END ELSE IF (sectorcurrent=sectornum) THEN BEGIN
              REPEAT
              UNTIL Readline(1) = timeout;
              Writeln(cr,lf,'Received duplicate sector ', sectorcurrent);
              Send(ack)
           END ELSE BEGIN
              Writeln(cr,lf,'Synchronization error');
              errorflag := true
           END
        END else BEGIN
           Writeln(cr,lf,'Sector number error');
           errorflag := true
        END
     END;
     IF errorflag THEN BEGIN
        inc(errors);
        REPEAT UNTIL Readline(1) = timeout;
        send(nak)
     END;
   UNTIL ((firstchar = Ord(EOT)) OR (firstchar = timeout)) OR
         (errors = errormax) OR (NOT Carrier);

   IF (firstchar = Ord(EOT)) AND (errors < errormax) THEN BEGIN
      send(ack);
      Writeln(cr,lf,'Transfer complete');
      errorcode := 0; recvxmodem := TRUE;
   end else if (errors > errormax) then begin
      Writeln(cr,lf,'Aborting');
      errorcode := 1;
   end else if not carrier then begin
      errorcode := 3;
   end;
end;


{====================================================================
 DOWNLOAD DISPATCHER
 ====================================================================}
function DownLoad(fname: string; using:protocols): boolean;
var result : boolean;
begin
   result := FALSE;
   case using of
      ascii : result := RecvAscii(fname);
      xmodemchksum : result := RecvXmodem(fname);
     {
     xmodemcrc     : result := RecvXmodemCRC(fname);
     }
   else
      write('protocol currently unavailable');
   end;
   DownLoad := result;
end;

{ initialization code }
begin
     checkbreak := false;
end.
