***************************************************************
; Combined Source Code for XMODEM and TXTMODEM
;   By Martin Eberhard
;
; XMODEM: Xmodem device driver for PTDOS
;   (Assemble with XMODM = TRUE)
;   Send/receive serial port data using Xmodem protocol.
;
; TXTMODEM: Text file Xmodem device driver for PTDOS
;   (Assemble with TXTMO = TRUE)
;   Send/receive ASCII files using Xmodem protocol.
;
;   On  reception, delete linefeeds and MSDOS EOFs.
;
;   On transmission, insert a linefeed after every CR
;   and pad last block with MS-DOS EOFs.
;
; Supports both checksum and CRC error checking.
;
; Assemble with Digital Research's ASM assembler.
;
;**************************************************************
; Note: PTDOS expects file lengths to be exact - there is no
; real EOF character, even for library files created by the
; save command. Unfortunately, the Xmodem protocol only allows
; 128-byte blocks - so every file will be a multiple of 128
; bytes. This program pads the end with 0's. This works fine
; with text files, and is harmless for programs, aside from
; making them a little longer. However, when you read a library
; file with GET, these pad characters at the end will cause a
; PTDOS error message on the console, though no harmful outcome
; results from this error.
;
; The Sol-20 LOCAL key will cause its serial port data output
; to be shunted to its input, rather than to the external
; connector. This code tests for the LOCAL key 'ON' by looking
; for serial port output echoed at its input at the beginning
; of the Xmodem transfer. If the LOCAL appears to be 'ON', the
; user is asked to turn it off.
;**************************************************************
VERSION	equ	102h	;BCD encoding
;
;  1.02   26 July 2023
;    Ported to assemble with Digital Research's ASM.
;    (No code changes)
;  1.02   02 March 2015 M. Eberhard
;    Improved LOCAL key message.
;        Still assembles with PT's ASSM
;  1.01   11 February 2013 Martin Eberhard
;    Created, assembles with PT's ASSM
;**************************************************************
TRUE	equ	0FFh
FALSE	equ	0

;====================================
; Assembly option: TXTMODEM or XMODEM
;  Exactly one of these must be TRUE
;====================================
TXTMO	equ	FALSE		;TRUE to create TXTMODEM
XMODM	equ	TRUE		;TRUE to create XMODEM

;------------------------------------------------------------------
 if TXTMO
PACFY	equ	TRUE		;Print progress pacifiers
 endif	;TXTMO

 if XMODM
PACFY	equ	FALSE		;Don't print progress pacifiers
 endif	;XMODM
;------------------------------------------------------------------
;===============
; SOLOS Equates
;===============
SOLOS	equ	0C000h		;SOLOS base address
AOUT	equ	1Ch		;write chr in b to pseudoport in a
AINP	equ	22h		;read from pseudoport a into a

;pseudoportS

KBDPP	equ	0		;keyboard
VDMPP	equ	0		;VDM
SERPP	equ	1		;serial
PARPP	equ	2		;parallel
USRPP	equ	3		;user-written

;===============
; PTDOS Equates
;===============
CONST	equ	0BC9Ch		;test console for waiting chr
CONIN	equ	0BCA2h		;get console chr into a
CONOT	equ	0BC9Fh		;send a to console

SYS	equ	0BCBCh		;system	call	
RESET	equ	20		;reset system command

;===============
; ASCII Equates
;===============
SOH	equ	1		;Start of Xmodem block
EOT	equ	4		;End Xmodem session
ACK	equ	6		;Xmodem block acknowledge
NAK	equ	15h		;Xmodem block negative ACK
CR	equ	0Dh
LF	equ	0Ah
ESC	equ	1Bh

;=================
; PROGRAM Equates
;=================
MSEOF	equ	1Ah		;MS-DOS EOF chr
ERLIM	equ	10		;max errors before giving up
BLKSZ	equ	128		;bytes/Xmodem block (must be 128)
CRTRY	equ	3		;times to try CRC before reverting to chksum

 if TXTMO
PADCH	equ	MSEOF		;text files get Padded with MS=DOS EOF
 endif ;TXTMO

 if XMODM
PADCH	equ	0		;pad binary with nulls
 endif ;XMODM

;Timeout values in seconds

SOHTO	equ	10		;sender to send SOH 
NAKTO	equ	90		;receiver to send initial NAK
ACKTO	equ	60		;receiver to ACK (or NAK)

;Progress pacifiers printed on the console

 if PACFY
PACACK	equ	'+'		;Received an ACK
PACNAK	equ	'-'		;Received a NAK
PACBLK	equ	'+'		;Received a good block
PACRSD	equ	'-'		;Requested a resend
 endif ;PACIFY

;===========================================================
; Block Transfer Buffer
; This must be on a 100h boundary and must be a multiple of
; 100H in size. The driver code follows immediately after
; BLKBF. This should be as high in memory as possible (while
; avoiding PTDOS) to leave plenty of room for a program like
; ACOPY or HEX2IMG that starts at 0000h,
;===========================================================
	org	0D000h 	;base address must end with 00

BLKBF:	dS	800h	;buffer size must end with 00
BBMAX	equ	$

;***********************************************
; PTDOS Device Driver Header                   *
; This must be the first code in the resulting *
; hex file, so that PTDOS will find it.        *
;***********************************************
MAIN	equ	$	;Main memory segment address

DTRB:	dw	DORB	;Read Block
DTRNB:	dw	DORB	;Read Next Block
DTRLB:	dw	0000h	;Read Previous Block (not supported)
DTWBR:	dw	DOWB	;Write Block then Read Next Block
DTWB:	dw	DOWB	;write Block
DTREW:	dw	DOREW	;Rewind
DTEOF:	dw	DOEOF	;Emd of File
DTCLO:	dw	DOCLO	;Close Driver
DTSEK:	dw	0000h	;Seek (not supported)
DTCTL:	dw	0000h	;Control/Status Request (not supported)
DTBLK:	dw	128 	;Max PTDOS transfer block size
DTITO:	db	0	;No immediate-transfer option
DTINI:	dw	DOINI	;Initialize driver

************************
;    RAM variables     *
; initialized BY DOINI *
************************
RABEG:	equ	$
BUFIP:	dw	0	;BLKBF in-pointer or general pointer
BUFOP:	dw	0	;BLKBF out-pointer
BUFBC:	dw	0	;BLKBF byte count
CRC16:	dw	0	;16-bit CRC
BLKNO:	dw	0	;Xmodem block number/count
RTRYS:	dw	0	;total number of retries
RXBLK:	db	0	;received block number
RXDFL:	db	0	;0 for send, 1 for receive
ERRCT:	db	0	;block error counter
EOFFL:	db	0	;<>0 when EOF needs to be sent
INIFL:	db	0	;<>0 means we already got 1st record
CRCFL:	db	0	;0 means TX/RX with checksUm, >0 means CRC
SAVSP:	dw	0	;sp save for RX abort
RAEND:	equ	$	;end of DOINI-initialized variables

;*********************************
; PTDOS Driver Function Routines *
;*********************************
;===========================================================
;= Driver Function = Initialize Driver
;===================
;On Entry:
;  no parameters
;  We don't know yet whether we will be sending or receiving
;On Exit:
;  return1 = normal return
;===========================================================
DOINI:	lxi	h,RABEG		;clear all RAM variables
	lxi	b,RAEND-RABEG	;clears b also

INILP:	mov	m,b
	inx	h
	dcr	C
	jnz	INILP

	lxi	h,BLKBF		;initialize BLKBF pointers
	shld	BUFIP
	shld	BUFOP

	mvi	a,CRTRY		;number of times to try CRC mode
	sta	INIFL 		;NZ means need to init for TX

	lxi	d,INIMS		;XMODEM or TXTMODEM banner
	call	PRNTF

	call	RXBY0		;Flush trash from serial port
	jmp	RXBY0		;In case it's a double-buffer

;==============================================================
;= Driver Function = Close Driver
;===================
; If we were sending, flush out whatever data remains in BLKBF,
; which will require padding out the  last Xmodem block. Then
; send an Xmodem EOF, and exit with a console message. If we
; were sending, just exit with a console
;  message.
;On Entry:
;  RXDFL = 1 if we were receiving
;On Exit:
;  if we were sending, then EOFFL <> 0
;     and all buffer data has been sent
;  return1 = normal return
;==============================================================
DOCLO:	lxi	d,RXSMS		;'received'

	lda	RXDFL		;sending or receiving?
	ora	a		;done already if receiving
 	jnz	CLDON

	lda	EOFFL		;Did we already send the EOF?
	ora	a
	rnz			;done if so

	lxi	h,0		;save sp in case of abort
	dad	sp
	shld	SAVSP

;---------------------------------------------
; Pad and then  send the last block, as needed
;---------------------------------------------
	lhld	BUFBC		;How many bytes left in BLKBF?
	mov	a,h
	ora	l
	jz	TXEOF		;None: send final EOF

;Pad data out to even BLKSZ boundary

	xchg			;de = byte count
	lhld	BUFIP

PADLP:	mvi	m,PADCH		;pad character
	inx	h
	inr	e
	mov	a,e		;enough?
	cpi	BLKSZ
	jnz	PADLP

	sta	BUFBC		;only low byte changed

	call	PDATA		;send padded last block

;----------------------------------------------
; EOF: send EOT'S to receiverm until we get ACK
;----------------------------------------------
TXEOF:	mvi	b,EOT		;send an EOT
	mov	a,b
	sta	EOFFL		;remember EOF'S been sent
	call	TXBYT

	call	GTACK		;Z set when we get an ACK
	jnz	TXEOF		;loop until we get one

;-------------------------------------------
; Termination message with error check mode,
; and with block and retry countS in decimal
;-------------------------------------------
	lxi	d,TXSMS		;'Done. Sent '

CLDON:	call 	PRNTF		;'Done. Sent ' or 'Received '

	lhld	BLKNO		;HOW many blockS sent/received?
	call	PRNTD		;binary to decimal print

	lxi	d,BLKMS		;'Xmodem/'
	call	PRNTF

	lxi	d,CRWMS		;'CRC blocks with '
	lda	CRCFL
	ora	a
	jnz	DON01
	lxi	d,CKWMS		;checksum blocks with '
DON01:	call	PRNTF

	lhld	RTRYS		;retry count
	call	PRNTD		;in decimal

	lxi	d,RTYMS		;' retries.'
	jmp	PRNTF		;print it and ret to PTDOS

;===========================================================
;= Driver Function = Read Block
;===================
; If this is the first block, then negotiate checksum or CRC
;   reception with the sender.
; If BLKBUF is empty, then get another BUffer-full from the
;   serial port.
; Transfer as much data as possible from BLKBUF to PTDOS
; If the last of the data has been sent to PTDOS, then tell
;   PTDOS 'EOF' by using return1
;On Entry:
;  hl = destination address in PTDOS
;  de = max bytes PTDOS can handle
;  INIFL > 0 number of times to try CRC mode
;          0FFh: not the first block
;On Exit:
;  return1 if EOF or abort, return2 otherwise
;  INIFL = 0FFh
;  hl = number of bytes read since call	
;On Entry and Exit:
;  CRCFL = 0 for checksum, >0 for CRC
;  EOFFL = 1 if EOF already received from sender
;  BUFIP = current address in BLKBF
;  BUFBC = remaining bytes in BLKBF
;  BLKNO = last block number received
;==========================================================
DORB:	mvi	a,1		;remember we are receiving
	sta	RXDFL

	push	d		;max PTDOS bytes
	push	h		;PTDOS address

	lxi	h,4		;There are 2 items on the stack now
	dad	sp		;calculate original sp
	shld	SAVSP		;remember sp in case we abort

;-----------------------------
; If BLKBF'S empty, go load it
;-----------------------------
	lhld	BUFBC		;buffer empty?
	mov	a,h
	ora	l
	cz	GDATA		;y: BLKBF. set Z if no data (EOF)

	pop	b		;PTDOS address
	pop	d		;PTDOS max bytes
	lxi	h,0		;byte count sent to PTDOS

	rz			;return1 if EOF and all data transfered

;--------------------------------------------------
; Loop to transfer data from BLKBF to PTDOS until
; BLKBF is empty or PTDOS buffer is full.
; EOFFL = 1 if EOF was already received from sender
;--------------------------------------------------
	push	h		;sent-byte-count onto stack
	lhld	BUFIP		;hl = BLKBF address

DORB1:	push	h		;save source address
	lhld	BUFBC		;Dec/test BLKBF byte count
	dcx	h
	mov	a,h		;underflow?
	inr	a
	jz	DORB0
	shld	BUFBC
DORB0:	pop	h		;source address
 	jZ	DORB2		;Z if we have emptied BLKBF

	XTHL			;bump PTDOS byte count
	inx	h		;...which is on the stack
	XTHL

	mov	a,m		;move a byte from BLKBF
	staX	b		;...TO PTDOS
	inx	h		;bump address pointers
	inx	b
	dcx	d		;Decrement PTDOS byte count

	mov	a,d		;still room in PTDOS buffer?
	ora	e		;...16-bit test
	jnz	DORB1		;go as long as we can

;----------------------------------------------
; Save state and return to PTDOS either because
; PTDOS'S buffer is full or BLKBUF is empty.
;   hl = current buffer address
;   TOP-of-stack = transfer byte count
;----------------------------------------------
DORB2:	shld	BUFIP		;save input buffer pointer
 
	pop	h		;hl = count of bytes transfered

	pop	d		;No EOF: get return address
	inx	d		;...make it return2
	inx	d
	inx	d
	push	d		;...and put it back
	ret			;with hl = bytes transfered to PTDOS

;==========================================================
;= Driver Function = End File
;===================
; Send last block of data from PTDOS
; Note: This does not send an Xmodem EOF, nor does it flush
; BLKBUF. (DTCLO does this.)
;On Entry:
;   hl = source buffer address
;   de = buffer size
;On Exit:
;  return1 = normal return
;On Entry and Exit:
;  BUFBC = number of bytes already in BLKBF
;  BUFIP = BLKBF in-pointer = address of empty next slot
;  BUFOP = BLKBF out-pointer = address of next byte to send
;  BLKNO = last Xmodem block number sent
;==========================================================
DOEOF:	lda	RXDFL		;sending or receiving?
	ora	a
	rnz			;done already if receiving

	call	DOWB		;write what came with call	
	ret			;In case of abort
	nop			;normal return 2 skips 3
	nop

	ret			;DTEOF must use return1

;===========================================================
;= Driver Function = Write Block
;===================
; Transfer de bytes of data frOm PTDOS BUffer to BLKBF. Then
; send as much as possible as Xmodem blocks. Quit when there
; is less than one Xmodem block (BLKSZ bytes) of data left
; in BLKBF. BLKBF's size is big enough to hold the max PTDOS
; transfer (defined at DTBLK) plus at least two more Xmodem
; blocks (BLKSZ).
;
;On Entry:
;  hl = PTDOS source buffer address
;  de = number of bytes to get from PTDOS
;On Exit:
;  return2 = normal return
;  hl = 0 = number of blockS read
;On Entry and Exit:
;  BUFBC = number of bytes already in BLKBF
;  BUFIP = BLKBF in-pointer = address of next slot
;  BUFOP = BLKBUF out-pointer = address of next byte to send
;  BLKNO = block number of the last block sent
;  CRCFL = 0 for checksum, >0 for CRC
;===========================================================
DOWB:	mov	a,d		;check for zero transfer bytes
	ora	e
	jz	DOWB5		;No bytes: done

	mov	b,h		;bc gets PTDOS address
	mov	c,l

	lxi	h,0		;remember sp in case we abort
	dad	sp		;calculate original sp
	shld	SAVSP

;-------------------------------------------
; Loop to get de bytes from PTDOS at bc into
; BLKBF at BUFIP. Guaranteed to have room.
; BLKBF is a circular queue during Tx.
;-------------------------------------------
	lhld	BUFIP		;hl = BLKBF in-pointer
 
DOWB1:	ldaX	b		;get PTDOS data
	inx	b		;bump PTDOS pointer

	push	b		;save pointer

DOWB2:	mov	c,a		;for CRLF test
	call	ENQUE		;put a in queue

 if TXTMO			;follow CRs with LFs in text files
	mov	a,c		;Was it a CR?
	cpi	CR
	mvi	a,LF
	cz	ENQUE		;y: follow it with an LF
 endif ;TXTMO

	pop	b		;PTDOS pointer

	dcx	d		;more PTDOS data?
	mov	a,d
	ora	e
	jnz	DOWB1

	shld	BUFIP		;save new in-pointer

;----------------------------------------
; Loop to send data as Xmodem blockS.
; stop when fewer than BLKSZ bytes remain
;----------------------------------------
DOWB3:	lhld	BUFBC		;BLKSZ bytes in buffer?
	mov	a,h
	ora	a
	jnz	DOWB4		;more than 256
	mov	a,l
	cpi	BLKSZ		;Carry set if BLKSZ > a
	jc	DOWB5

DOWB4:	call	PDATA		;send an Xmodem block

	jmp 	DOWB3

;-----------------
; Normal DOWB Exit
;-----------------
DOWB5:	pop	h		;Fix stack for return2
	inx	h
	inx	h
	inx	h
	push	h

;Fall into DOREW

;=========================================
;= Driver Function = Rewind
;===================
; PTDOS Doc Says Sequential Device driverS
; should just return with hl = 0
;On Entry:
;  Don't care
;On Exit:
;  return1 = normal return
;  hl = 0
;=========================================
DOREW:	lxi	h,0
	ret

***************
; SubroutineS *
***************
;===Subroutine=====================
; Put a in queue and circular-
; increment hl, and increment BUFBC
;Trashes psw
;==================================
ENQUE:	mov	m,a		;put byte in BLKBF

	push	h		;bump buffer count
	lhld	BUFBC
	inx	h
	shld	BUFBC
 	pop	h

;Fall into CINCH
;===Subroutine==================
; circular-increment hl
;Assumes 256-byte aligned buffer
;Trashes psw
;===============================
CINCH:	inx	h		;circular-increment BLKBF pointer
	mov	a,h
	cpi	BBMAX/256	;256-byte aligned buffer
	rnz

	mvi	h,BLKBF/256
	ret

;===Subroutine===================================
; Send one BLKSZ-Byte Xmodem Block
;On Entry:
;  INIFL <> 0 if we haven't sent any blockS yet
;  CRCFL = 0 for checksum, >0 for CRC
;  BLKBF = circular queue with data
;  BUFBC = number of bytes in BLKBF
;  BUFOP = address of next byte to send
;  BLKNO = block number of previous Xmodem block
;On Exit:
;  INIFL = 0
;  CRCFL = 0 for checksum, >0 for CRC
;  BUFBC = BUFBC-BLKSZ
;  BUFOP = address of next byte to send
;Trashes psw,bc,de,hl
;================================================

;---------------------------------------------
; If this will be the first Xmodem block, then
; wait for either 'C' or NAK from receiver, to
; select CRC or checksum transmission.
;---------------------------------------------
PDATA:	lda	INIFL		;need to negotiate?
	ora	a 
	jz	PDAT3		;NZ: already know CRC or checksum

PDAT0:	mvi	b,0		;test for LOCAL key
	call	TXBYT		;send a null to see

	mvi	c,NAKTO		;long timeout

PDAT1:	dcr	c		;timeout?
	jz	NAKAB		;y: abort

	call	RXBY1		;will return 0 if timeout
	jc	PDAT1		;timeout?

	ora	a		;0 but no timeout?
	cz	lOCAL		;looks like LOCAL key is on
	jz	PDAT0

	cpi	NAK		;checksum mode?
	jz	PDAT2		;checksum mode enabled

	xri	'C'		;CRC mode?
	jnz	PDAT1		;unknown chr

	inr	a		;a=1
	sta	CRCFL		;remember mode

PDAT2:	xra	a		;done with negotiation
	sta	INIFL

 if PACFY
	lxi	d,TRNMS		;Transitting with
	call	PMODE		;print checksums or CRC
 endif ;PACFY

;----------------
; Send next block
;----------------
PDAT3:	lhld	BLKNO		;inc 16-bit block count
	inx	h
	shld	BLKNO

	xra	a		;No error in this block yet
	sta	ERRCT

;-----------------------------------------
; Send the 3-byte Block Header: 
; SOh, block number, inverted block number
;-----------------------------------------
PDAT4:	mvi	b,SOH		;1) SOH
	call	TXBYT

	lxi	h,BLKNO		;2) 8-bit block number
	mov	b,m
	call	TXBYT

	mov	a,m
	cma			;3) Inverse block number
	mov	b,a
	call	TXBYT

;--------------------------------------------
; Send BLKSZ bytes from BLKBF. Calculate the
; checksum in c and the  16-bit CRC in CRC16.
;--------------------------------------------
	lxi	h,0		;clear CRC
	shld	CRC16
	mov	c,h		;...clear checksum in c

	lhld	BUFOP		;point to next data
	mvi	b,BLKSZ		;loop counter  

PDAT5:	push	b		;loop counter, checksum

	mov	b,m		;get byte from BLKBF
	call	TXBYT		;send chr from b

	mov	a,b		;compute CRC
	call	CACRC		;returns CRC in de

	mov	a,b 		;get chr for checksum
 	pop	b		;loop counter, checksum

	add	c		;compute checksum in c
	mov	c,a

	call	CINCH		;circular inc hl
	dcr	b		;ALL bytes sent?
	jnz	PDAT5		;loop through all bytes

;-----------------------------------------
; Send the checksum or CRc, based on CRCFL
;   c = checksum
;   de = 16-bit CRC from CALCRC
;-----------------------------------------
	lda	CRCFL		;checksum or CRC?
	ora	a
	jz	PDAT7		;Z: send checksum

	mov	b,d		;High CRC byte 1st
	call	TXBYT

	mov	c,e		;Now 2nd CRC byte

PDAT7:	mov	b,c		;checksum or 2nd CRC byte 
	call	TXBYT

;------------------------------------------------
; Wait for an ACK. if NAK or timeout, then resend
;------------------------------------------------
	call	GTACK		;wait for Z set = ACK
	jnz	PDAT8		;NZ: timeout or NAK

;------------------------------
; ACK received. Update pointers
;------------------------------
	shld	BUFOP		;Updated out-pointer

	lhld	BUFBC		;Update byte count
	mov	a,l
	sui	BLKSZ
	mov	l,a
	mov	a,h
	sbi	BLKSZ/256	;borrow if needed
	mov	h,a
	shld	BUFBC

;--------------------------
; Print pacifier and return
;--------------------------
 if PACFY
	mvi	a,PACACK 	;Happy pacifier
	call	CONOT		;return from there
 endif ;PACFY

	ret

;----------------
; Retry the block
;----------------
PDAT8:	lhld	RTRYS
	inx	h
	shld	RTRYS
	jmp	PDAT4		;retry it now

;===Subroutine==================
; Get an ACK from the Receiver
;On Exit:
;  Carry set IFF timeout
;  Z set IFF ACK received
;  ERRCT incremented on timeout
;  if too many errors, abort
;Trashes a,b
;===============================

;-----------------------------------------------
; Delete any noise-induced bogus characters that
; might arrive long before ACK/NAK would arrive
;-----------------------------------------------
GTACK:	call	RXBY0
	call	RXBY0		;Suck UART dry

;---------------------------------
; Get an Xmodem byte, test for ACK
;---------------------------------
	mvi	b,ACKTO*2	;ACK timeout value
	call	RXBYT		;get Xmodem byte
	jc	GTAC1		;Carry means timeout

	cpi	ACK
	rz			;ACK: return with Z set

;-------------------------------------
; If NAK, then print the NAK pacifier,
; and return with Z and Carry cleared
;-------------------------------------
	cpi	NAK		;NAK?
	jnz	GTAC1		;NZ: bogus chr

 if PACFY
	mvi	a,PACNAK	;NAK pacifier
	call	CONOT
 endif ;PACFY

	ora	a		;NAK: clear Z and Carry
	ret	

;--------------------------------------------------
; Timeout or bogus character. bump and check ERRCT.
; Abort if too many, Otherwise set Carry and return
;--------------------------------------------------
GTAC1:	lda	ERRCT
	inr	a
	sta	ERRCT		;too many errors?
	cpi	ERLIM
	rc			;n: ret w/ Carry set, Z clear

;-----------------------
; Abort: too many errors
;-----------------------
	lxi	d,AERMS		;too many ACK errors
	jmp	ABORT

;===Subroutine===================================
; Get a BUffer-Full of Data from the Sender
;On Entry:
;  If first Xmodem block:
;     INIFL = number of times to try CRC mode (>0)
;  For all further Xmodem blocks:
;     INIFL = 0FFh if not the first block
;     The previous block needs to be ACK'd
;On Exit:
;  INIFL = 0FFh unless error in 1st block
;  BUFIP = BLKBF
;  BUFBC = number of bytes in buffer
;  unless we got an EOF, the last block
;  has not been ACK'd
;  Z set if buffer is still empty (EOF)
;On Entry and Exit:
;  CSMFL = 0 for checksum, 1 for CRC
;  EOFFL = 1 if EOF received from sender
;  BLKNO = last block number received
;Trashes a,bc,de,hl
;================================================
GDATA:	lda	EOFFL		;got EOF already?
	xri	1
	rz			;y: return with Z

	lxi	h,BLKBF		;point to buffer beginning
	shld	BUFIP

;-------------------------------------------------
; Send ACK for previous block, unless this is the
; 1st block (INIFL<>0FFh). For the 1st block, send
; 'C' to request CRC transmission. after CRTRY
; tries, send NAK to request checksum transmission
;-------------------------------------------------
GDA01:	xra	a		;clear error count
	sta	ERRCT

	mvi	b,ACK

GDA02:	lxi	h,INIFL		;1st block?
	mov	a,m
	inr	a		;test for INFL=0FFh (not 1st block)
	jz	GDA04		;n: send ACK (OR NAK on retry)

	mvi	b,NAK
	dcr	a		;checksum mode?
	jz	GDA03

	mvi	b,'C'		;still trying CRC
	dcr	m		;count down 'C'	retries

GDA03:	sta	CRCFL		;checksum or CRC mode

GDA04:	call	TXBYT		;send b as ACK

;---------------------------------------
; Wait for SOH to start reception. If we
; get an EOT instead, then we are done.
;---------------------------------------
	mvi	b,SOHTO*2	;timeout for SOH
	call	RXBYT
	jc	GDA06		;Carry means timeout

	cpi	SOH		;Did we get the SOH?
	jz	GDA07		;y: get the block

	xri	EOT		;set EOFFL and end if EOT
	jnz	GDA05		;otherwise, header error

	inr	a		;a=1
	sta	EOFFL		;remember that we got EOF

	mvi	b,ACK		;ACK the EOF
	call	TXBYT

	jmp	GDA10		;done receiving

;------------------------------------------
; No SOH or EOT: bogus chr. If we are still
; sending 'C' then see if we got 'C' back.
; if so, the LOCAL key is probably on. 
;   hl = INIFL here
;------------------------------------------
GDA05:	cpi	47h		;= 'C' XOR EOT
	jnz	GDA51

	mov	a,m		;INIFL
	cpi	CRTRY-1		;first time trying 'C'?
	jnz	GDA51

	inr	m		;Undo 'C' try

	call	LOCAL		;ask user to turn it off
	jmp	GDA01		;Try again

;--------------------------------
; Some sort of bogus input. Purge
; until the serial port is quiet.
;--------------------------------
GDA51:	call	RXBY1		;Carry set on timeout
	jnc	GDA51		;Purge until timeout

;Fall into GDA06

;-----------------------------------------
; Bump counters, print pacifier, and retry
;-----------------------------------------
GDA06:	lxi	h,ERRCT
	inr	m		;bump error count

	lhld	RTRYS		;bump retry counter
	inx	h
	shld	RTRYS

 if PACFY
	mvi	a,PACRSD	;error pacifier
	call	CONOT
 endif ;PACFY
 
	mvi	b,NAK		;retry request

	mov	a,m
	cpi	ERLIM		;too many errors?
	jc	GDA02		;n: try again with B=NAK

;-----------------------
; Abort: too many errors
;-----------------------
	lxi	d,ECEMS		;too many errors
	jmp	ABORT

;------------------------------------
; Got SOH. Now get rest of header:
; block number, inverted block number
;   hl = INIFL here
;------------------------------------
GDA07:	mvi	m,0FFh		;We got an SOH

	lxi	h,0		;initialize CRC
	shld	CRC16

	lhld	BUFIP

	call	RXBY1		;get block number
	jc	GDA06		;timeout?

	mov	d,a

	call	RXBY1		;Inverted block number
	jc	GDA06		;timeout?

	cma			;Invert to Compare
	cmp	d
	jnz	GDA51		;No match: error

	sta	RXBLK		;save block number

;-----------------------------------
; Receive BLKSZ bytes into BLKBF.
; compute Checksum and CRC as we go.
;-----------------------------------
	lxi	b,BLKSZ*256+0	;b=bytes, c=0 checksum
	lhld	BUFIP		;Point to BUffer

GDA08:	push	b		;byte count and checksum
	call	RXBY1		;get 1 data byte
 	pop	b
	jc	GDA06		;timeout?

 if TXTMO ;eat LFS in text mode
	cpi	MSEOF		;eat any pad chrs at end
	jz	GDA18

	cpi	LF		;Chuck LFs
	jz	GDA18
 endif ;TXTMO

	mov	m,a		;store data in BLKBF

GDA28:	inx	h		;next byte

GDA18:	mov	d,a		;Temp save for CRC
	add	C		;compute checksum
	mov	c,a
	mov	a,d		;get data byte back

	call	CACRC		;Calculate CRC too, in de

	dcr	b		;done yet?
	jnz	GDA08

;-------------------------------------------------
; Verify checksum in c or CRC in de based on CRCFL
;-------------------------------------------------
	lda	CRCFL		;CRC mode?
	ora	a		;0 means checksum
	jz	GDA09

	call	RXBY1		;get 1st CRC byte
	jc	GDA06		;timeout?

	cmp	d		;CRC first byte match?
	jnz	GDA51		;n: purge data and try again

	mov	c,e		;C=2nd CRC byte

GDA09:	call	RXBY1		;2nd CRC byte or checksum
	jc	GDA06		;timeout?

	cmp	C		;match?
	jnz	GDA06		;n: error

;-------------------------------------------------
; If we just got the 1st block, then we now know
; the error-check mode. Print console message now.
;-------------------------------------------------
 if PACFY
 	push	h
	lhld	BLKNO		;still at initial value? 
	mov	a,h
	ora	l
 	pop	h

	lxi	d,RCVMS		;receiving with
	cz	PMODE		;print checksums or CRC
 endif ;PACFY

;---------------------------------------------------------
; Good block. Did we already get this block? (Might be a
; retransmission.) if it'S the same number as the previous
; block, ignore it. if it'S the next block, keep going.
; anything else is a sync error.  
;---------------------------------------------------------
	xchg			;BLKBF pointer to de
	lhld	BLKNO		;Prior 16-bit block number

	lda	RXBLK		;a=this block number
	sub	l		;8-bit block number
	jz	GDA01		;..send ACK and ignore

	dcr	a		;should be next block
	jnz	SYERR

	inx	h		;bump 16-bit BLKNO
	shld	BLKNO
	xchg			;BLKBF pointer to hl

;--------------------------------------------------------
; Got a good block. Print pacifier (if enabled). If there
; is room in the buffer, get more data. This assumes the
; buffer is on a 256-byte boundary, and that max data in
; a block (including inserted LF'S) <= 256 bytes
;--------------------------------------------------------
	shld	BUFIP		;save buffer address

 if PACFY
	mvi	a,PACBLK	;good block pacifier
	call	CONOT
 endif ;PACFY

	mov	a,h
	cpi	BBMAX/256-1	;Simple check
	jnz	GDA01

;-------------------------------------
; BLKBF is full. calculate byte count,
; reset buffer pointer and return
;-------------------------------------
GDA10:	lhld	BUFIP
	mov	a,h		;BUFBC := BUFIP-BLKBF
	sui	BLKBF/256
	mov	h,a
	shld	BUFBC		;BUFBC is current

	ora	l		;Set Z if buffer is empty

	lxi	h,BLKBF		;point to start of BLKBF
	shld	BUFIP

	ret			;with Z set if EOF found

;--------------------------------
; SYNC error: blockS out of order
;--------------------------------
SYERR:	lxi	d,SERMS		;Sync error
	jmp	ABORT

;===Subroutine======================
; Update 16-bit CRC with a
;On Entry:
;  a = new data byte
;  CRC16 is current for prior bytes
;On Exit:
;  CRC16 updated
;  de = CRC so far
;Trashes psw
;(571 cycles = ~285 uS)
;===================================
CACRC:	push	h		;(11)
	lhld	CRC16		;(16)
 
	xra	h		;(4)XOR into top byte
	mov	h,a		;(5)

	mvi	d,8		;(7)Prepare for 8 rotations

CCRC1:	dad	h		;(10)16-bit shift left
	jnc	CCRC2		;(10)Skip if bit 15 was 0

	mov	a,h		;(5)CRC = CRC XOR 1021h
	xri	10h		;(7)
	mov	h,a		;(5)
	mov	a,l		;(5)
	xri	21h		;(5)
	mov	l,a

CCRC2:	dcr	d		;(4)
	jnz	CCRC1		;(10)Rotate 8 times

;61X8=488 cycles

	shld	CRC16		;(16)save CRC so far

	xchg			;(4)result in de for return
 	pop	h		;(10)
	ret			;(10)

;===Subroutine========================
; Receive a Byte from the Serial Port
; Abort if the User types ESC
;On Entry at RXBY1:
;  1-Second timeout
;On Entry at RXBYT:
;  b = timeout in units of 1/2 SEC
;On Exit:
;  a = received chr
;  b = remaining time in timeout
;  Carry set and a=b=0 if timeout
;=====================================
RXBY1:	mvi	b,2		;1-second timeout

RXBYT:	push	h
 	push	d

;------------------------------------------------
; Loop until timeout, check for ESC every 1/2 sec
;------------------------------------------------
RXW1:	push	b		;(11)
	call	CONST		;(17+141)user type anything?
	cnz	CKESC		;(11)go see what was typed
 	pop	b		;(10)

;----------------------------------
; Inner loop: 201 cycles = 100.5 uS
; 0.5 sec / 100.5 uS = 4975 cycles
;----------------------------------
	lxi	d,4975		;1/2-second count down

RXW2:	call	RXBY0		;(17+160)receive chr into a
	jnz	RXW3 		;(10)got a chr?
	dcx	d		;(5)timeout timer
	mov	a,d		;(5)
	ora	e		;(4)
	jnz	RXW2		;(10)

	dcr	b
	jnz	RXW1

;------------------------------
; Timeout: set Carry and return
;------------------------------
	stc
 	pop	d
 	pop	h
	ret

;-------------------------
; Return with RX byte in a
;-------------------------
RXW3:	ora	a		;clear carry
 	pop	d
 	pop	h
	ret

;===Subroutine========================
; Receive One Serial Port Byte
;On Exit:
;  a = chr and Z cleared if chr ready
;  a = 0 and Z set if no chr
; (160 US)
;=====================================
RXBY0:	mvi	a,SERPP		;(7)serial pseudoport
	jmp	SOLOS+AINP	;(17+136)Z set if no chr

;===Subroutine============================
; Print Error-Checking Mode on the Console
;On Entry:
;  de points to initial string
;  CRCFL = 0 for checksums, <>0 for CRCs
;Trashes psw,de
;=========================================
 if PACFY
PMODE:	call	PRNTF		;sending with or receiving with

	lxi	d,CKSMS		;checksums
	lda	CRCFL
	ora	a
	jz	PRNTF		;return via PRNTF
	lxi	d,CRCMS		;CRCS
	jmp	PRNTF		;return via PRNTF
 endif ;PACFY

;===Subroutine=============
; Send b to the Serial Port
;On Entry:
;  b = chr to print
;Trashes psw
;==========================
TXBYT:	mvi	a,SERPP		;pseudoport in a
	jmp	SOLOS+AOUT

;===Subroutine==========================
; Ask the User to Turn the LOCAL Key Off
;On Exit:
;  Z is set
;=======================================
LOCAL:	lxi	d,LKYMS		;'Turn off LOCAL key. Ready (Y/N)?'
	call	PRNTF		;...'readY (Y/N)?'

	call	CONIN		;wait for a key
	ani	05Fh		;convert to uppercase
	mov	c,a

	lxi	d,CRLFM		;print CR LF
	call	PRNTF
 
	mov	a,c		;what got typed?
	cpi	'Y'
	rz			;Y means ready
	cpi	'N'
	jnz	lOCAL

	call	SYS
 	db	RESET		;PTDOS will not return from this

;===Exit============================================
; Terminate Due to no Initial NAK or 'C' from sender
;===================================================
NAKAB:	lxi	d,NNKMS
	jmp	ABORT

;===Subroutine========================
; Check for ESC from the User
; Abort if ESC typed, ignore all else.
;Trashes psw
;=====================================
CKESC:	call	CONIN		;get the chr
	ani	7Fh		;(7)Strip parity
	cpi	eSC		;(7)ESCAPE means quit
	rnz			;(11)Ignore all else

	lxi	d,UABMS		;user abort

;Fall into abort

;===Exit================================================
; Print Abort Message, and then Return Directly to PTDOS
;On Entry:
;  de = message address
;  SAVSP = return1 address to PTDOS
;=======================================================
ABORT:	lhld	SAVSP		;Restore PTDOS ret address
	sphl

;Fall into PRNTF, return from THERE

;===Subroutine==========================
; Print a Message on the Console
;On Entry:
;    de = address of 0-terminated string
;Trashes psw,de
;=======================================
PRNTF:	ldaX	d
	ora	a
	rz			;null termination?

	call	CONOT		;a to console
	inx	d
	jmp	PRNTF

;===Subroutine======================
; Print hl in Decimal on the Console
;Trashes all registers
;===================================
PRNTD:	mvi	d,0		;Suppress leading zeros
	lxi	b,-10000
	call	DIGIT
	lxi	b,-1000
	call	DIGIT
	lxi	b,-100
	call	DIGIT
	lxi	b,-10
	call	DIGIT

	mov	a,l		;least significant digit is easy
	jmp	DIG0		;..and is never suppressed

;---Local Subroutine--------------------------
; Divide hl by a Power of 10 and print result,
; unless it'S a leading 0
; returnS remainder in hl
; d = 0 if previous digits were all 0
;On Entry:
;  hl = vslue to divide
;  bc = degative power of 10
;  d = 0 IFF all prior digits were 0
;On Exit:
;  integer quotent has been printed unless it
;     is zero and d was zero
;  hl = remainder
;  d = 0 IFF this and prior digits are 0
;---------------------------------------------
DIGIT:	mvi	a,0FFh		;will go one too many times
 	push	d		;save leading 0 suppression

DIGLP:	mov	d,h		;DE gets previous value
	mov	e,l 
	inr	a 
	dad	b		;subtract a POWER of 10
	jc	DIGLP		;loop until borrow
	xchg 			;hl = remainder

 	pop	d		;leading 0 suppression
	mov	e,a		;save digit for printing
	ora	d		;still leading zeros?
	mov	d,a		;save for next digit
	rz			;done if still suppressing

	mov	a,e
DIG0:	adi	'0'		;make it ASCII
	jmp	CONOT		;print on console and return


***************************
* Null-Terminated Strings *
***************************
INIMS:	db	CR

 if TXTMO
	db	'PTDOS TXTMODEM vers. 1.02 for text files by M. Eberhard'
	db	CR,'   CR <--> CR/LF'
	db	CR,' NULL <--- MSDOS EOF'
 endif ;TXTMO

 if XMODM
	db	'PTDOS XMODEM 1.02 by M. Eberhard'
 endif ;XMODM

	db	CR,'Press ESCAPE to abort.',CR,0

 if PACFY
RCVMS:	db	'Receiving with ',0
TRNMS:	db	'Sending with ',0
CKSMS:	db	'checksums ',0
CRCMS:	db	'CRCs ',0
 endif ;PACFY

LKYMS:	db	CR,'Turn off LOCAL key. Ready (Y/N)?',0
UABMS:	db	CR,'ABORT',CR,0
SERMS:	db	CR,'ABORT: Synchronization error',CR,0
ECEMS:	db	CR,'ABORT: Error limit exceded',CR,0
AERMS:	db	CR,'ABORT: Too many ACK errors',CR,0
NNKMS:	db	CR,'Timeout waiting for sender',CR,0
TXSMS:	db	CR,'Done. Sent ',0
RXSMS:	db	CR,'Done. Received ',0
BLKMS:	db	' Xmodem/',0
CRWMS:	db	'CRC blocks with ',0
CKWMS:	db	'checksum blocks with ',0
RTYMS:	db	' retries.'
	db	CR,LF  			;FALL INTO CRLFM
CRLFM:	db	CR,LF,0


 end

