; MXC-ZS10.Z80
;
;       MEX-Plus Clock Overlay for ZSDOS
;
; A hardware independent clock module, obtaining the time and date
; from ZSDOS.
;
; This module passes the current ZSDOS time and date to MEX for
; display.  It also implements the CSET command to set the ZSDOS
; clock.  There are two separate commands:
;
;       CSET DATE {mm}/{dd}/{yy}
;
; and
;
;       CSET TIME {hh}:{mm}:{ss}
;
; Entering all elements of a date or time specification is not
; necessary.  Missing elements will be filled in from the current
; setting of the clock.  In other words, if you just want to change
; the hour--perhaps to daylight saving time--enter:
;       CSET TIME hh
; To adjust the minutes, use:
;       CSET TIME :mm
; Or to tweek the seconds:
;       CSET TIME ::ss
; The date parameter works the same way.  As the examples indicate,
; leading delimiters are required, but trailing delimiters are not.
;
; If desired, date entry can be configured to use European format:
;       CSET DATE dd.mm.yy
; See the EURDAT equate below.
;
; If ZSDOS returns no date and time, this clock module returns
; 12:00 a.m., January 1, 1978.  Relative time is not supported.
;
; Please report any bugs.
;       Gene Pizzetta
;       481 Revere St.
;       Revere, MA 02151
;
;       Voice:  (617) 284-0891
;       Newton Centre Z-Node:  (617) 965-7259
;       Ladera Z-Node Central:  (213) 670-9465
;
; Version 1.0 -- June 16, 1991 -- Gene Pizzetta
;       Based on MXC-DS10 overlay for DateStamper by Jim Lill, ZSLIB
;       routines by Carson Wilson, and SYSLIB routines by Richard
;       Conn.
;
; System addresses
;
Bdos    equ     0005h
TPA     equ     100h
;
; BDOS functions
;
ZSGTime equ     98              ; ZSDOS get time function
ZSSTime equ     99              ; ZSDOS set time function
;
; MEX functions
;
MEX     equ     0D00h           ; MEX function entry
LOOKUP  equ     247             ; table search: see CMDTBL comments for info
SBLANK  equ     244             ; scan input stream to next non-blank
GNC     equ     241             ; get char from input, cy=1 if none
ILP     equ     240             ; inline print
PRINT   equ     9               ; simulated BDOS function 9: print string
;
; ASCII
;
BELL    equ     07h
LF      equ     0Ah
CR      equ     0Dh
;
FALSE   equ     0
TRUE    equ     not FALSE
;
; Date format -- Select here either American (mm/dd/yy) or European
; (dd.mm.yy) date format for entry of date specification to CSET
; processor.  MEX always displays the date in American format.
;
EURDAT  equ     FALSE           ; FALSE=American, TRUE=European
;
       org     TPA             ; we begin
;
       db      0C3h            ; JP required by load
;
; Jump table for clock overlay
;
       org     0E00h           ; start of clock overlay

Start:  jp      GetTim          ; get time
       jp      GetDat          ; get date
       jp      CstCmd          ; cset processor
       ret                     ; clock overlay init
       db      0,0
       ret                     ; clock overlay de-init
       db      0,0
;
; GetTim -- gets BCD time into DatTbl and then loads registers with
; binary equivalents for MEX:
;       B = hours (0-23)
;       C = minutes (0-59)
;       D = seconds (0-59)
;       E = hundredths of seconds (0-99)
;
GetTim: call    GetZST          ; get time from ZSDOS
       jr      z,GetTm1        ; (okay, continue)
       ld      bc,0            ; no time, return midnight
       ld      de,0
       ret
;
GetTm1: ld      a,(hours)       ; BCD hours to A
       call    BcdBin
       ld      b,a             ; binary hours to B
       ld      a,(minute)      ; BCD minutes to A
       call    BcdBin
       ld      c,a             ; binary minutes to C
       ld      a,(second)      ; BCD seconds to A
       call    BcdBin
       ld      d,a             ; binary seconds to D
       ld      e,0             ; no 100th's of second
       ret
;
; GetDat -- gets BCD date into DatTbl and then loads registers with
; binary equivalents for MEX:
;       BC = year (1978-2077)
;       D  = month (1=Jan, 2=Feb, etc.)
;       E  = day (1-31)
;
GetDat: call    GetZST          ; get date from ZSDOS
       jr      z,GetDt1        ; (okay, continue)
       ld      e,1             ; no date, return Jan. 1, 1978
       ld      d,e
       ld      bc,1978
       ret
;
GetDt1: ld      a,(day)         ; BCD day to A
       call    BcdBin
       ld      e,a             ; binary day to E
       ld      a,(month)       ; BCD month to A
       call    BcdBin
       ld      d,a             ; binary month to D
       ld      a,(year)        ; BCD year (00-99) to A
       call    BcdBin
       ld      c,a             ; binary year in C
       ld      b,0
       ld      hl,1900         ; add century
       cp      78
       jr      nc,GetDt2
       ld      hl,2000
GetDt2: add     hl,bc
       ld      c,l             ; put year in BC
       ld      b,h
       ret
;
; CSET Processor for ZSDOS
;
CstCmd: ld      c,SBLANK        ; any arguments?
       call    MEX
       jr      c,CstErr        ; if not, display defaults
       ld      de,CstTbl
       ld      c,LOOKUP
       call    MEX             ; parse the argument
       push    hl              ; save any parsed arguments
       ret     nc              ; ..and return to it
       pop     hl              ; not found
CstErr: ld      de,CstEms
       ld      c,PRINT
       call    MEX
       ret
;
CstEms: db      BELL,'  CSET syntax error -- "CSET ?" for help',CR,LF,'$'
;
CstTbl: dc      '?'             ; help
       dw      CstHlp
       dc      'DATE'          ; set date
       dw      CsDate
       dc      'TIME'          ; set time
       dw      CsTime
       db      0               ; end of table
;
; CSET with "?" prints help message
;
CstHlp: call    SILP
       db      '  CSET Options:',CR,LF
       db      '    DATE      '
 IF EURDAT
       db      '{dd}.{mm}.{yy}'
 ELSE
       db      '{mm}/{dd}/{yy}'
 ENDIF ; EURDAT
       db      '    (sets ZSDOS date)',CR,LF
       db      '    TIME      {hh}:{mm}:{ss}    (sets ZSDOS time)',CR,LF
       db      0
       ret
;
; CsDate -- sets ZSDOS system date
;
CsDate: call    MvDat           ; move date to storage
       jp      c,CstErr        ; (none given)
       call    GetZST          ; fill DatTbl
       jr      nz,ClkErr       ; (clock error)
       call    PrsDat          ; parse date and make BCD
       jp      nz,CstErr       ; (invalid date)
       call    SetZST          ; set date
       jr      nz,ClkErr       ; (clock error)
       call    SILP
       db      'ZSDOS Date Set',0
       ret
;
; CsTime -- sets ZSDOS system time
;
CsTime: call    MvDat           ; move time to storage
       jp      c,CstErr        ; (none given)
       call    GetZST          ; fill DatTbl
       jr      nz,ClkErr       ; (clock error)
       call    PrsTim          ; parse time and make BCD
       jp      nz,CstErr       ; (invalid time)
       call    SetZST          ; set time
       jr      nz,ClkErr       ; (clock error)
       call    SILP
       db      'ZSDOS Time Set',0
       ret
;
ClkErr: call    SILP
       db      BELL,'ZSDOS clock error',0
       ret
;
; PrsDat -- parses date specification string.  (Modified from ZSLIB's
; ZSPARSDS module by Carson Wilson.)
;
PrsDat: ld      de,DatTbl       ; point to BCD date
       ld      hl,DatStr-1     ; point to command line date
       push    de
       ld      b,d
       ld      c,e
       inc     bc              ; BC --> storage + 1 (month)
 IF EURDAT
       inc     bc              ; BC --> storage + 2 (day)
 ENDIF ; EURDAT
;
; Test month (or day if EurDat)
;
       call    GetNxt          ; get next datespec character or abort
       cp      '.'             ; got character, use default month?
       jr      z,TestDy        ; (yes)
       cp      '/'
       jr      z,TestDy
       call    IsDgt           ; digit?
       jp      nz,ErExit       ; (no)
       call    eval16          ; must be day spec.  SYSLIB evaluates ASCII
                               ; ..hex to binary and points HL to next
TstMon: ld      (bc),a          ; save value
       ld      a,(hl)          ; get next
       cp      '.'             ; day spec?
       jr      z,TestDy
       cp      '/'
       jp      nz,PsExit       ; no, done
;
TestDy:
 IF EURDAT
       dec     bc              ; BC --> storage + 1 (month)
 ELSE
       inc     bc              ; BC --> storage + 2 (day)
 ENDIF ; EURDAT
       call    GetNxt          ; get/abort
       cp      '.'             ; got character, use default day?
       jr      z,TestYr        ; (yes)
       cp      '/'
       jr      z,TestYr
       call    IsDgt           ; digit?
       jp      nz,ErExit       ; (no)
       call    eval16          ; evaluate day
TestD1: ld      (bc),a          ; save value
       ld      a,(hl)
       cp      ' '
       jr      z,PsExit
       cp      '.'             ; got year?
       jr      z,TestYr
       cp      '/'
       jr      nz,PsExit       ; (no)
;
TestYr: dec     bc              ; point to year
 IF NOT EURDAT
       dec     bc
 ENDIF ; NOT EURDAT
       call    GetNxt          ; get/abort
       cp      ' '             ; use default year?
       jr      z,PsExit        ; (yes)
       call    IsDgt           ; digit?
       jr      nz,ErExit       ; (no)
       call    eval16          ; evaluate year
TestY1: ld      (bc),a          ; save value
       jr      PsExit          ; (no, done)
;
; PrsTim -- parses time specification string
;
PrsTim: ld      bc,Hours
       ld      de,DatTbl
       ld      hl,DatStr-1
       push    de
TestHr: call    GetNxt          ; get next command character
       cp      ':'             ; use default hour?
       jr      z,TestMn        ; (yes)
       call    IsDgt           ; digit?
       jr      nz,ErExit       ; (no, error)
       call    eval16          ; get hour
       ld      (bc),a          ; save value
       ld      a,(hl)
       cp      ':'             ; got minute?
       jr      nz,PsExit       ; (no, done)
;
TestMn: inc     bc              ; point to minute
       call    GetNxt          ; minute or wildcard
       cp      ':'             ; use default minute?
       jr      z,TestSc        ; (yes)
       cp      ' '
       jr      z,PsExit
       call    IsDgt           ; digit?
       jr      nz,ErExit       ; (no)
       call    eval16          ; evaluate minute
       ld      (bc),a          ; save value
       ld      a,(hl)
       cp      ':'             ; got second?
       jr      nz,PsExit       ; (no, done)
;
TestSc: inc     bc              ; point to second
       call    GetNxt          ; second
       cp      ' '
       jr      z,PsExit
       call    IsDgt           ; digit?
       jr      nz,ErExit       ; (no)
       call    eval16          ; evaluate second
       ld      (bc),a          ; save value
;
PsExit: pop     de              ; point to stored date
       ex      de,hl           ; check value at HL
       call    IsBcdd
       ex      de,hl           ; restore DE
       ret                     ; return (Z) if date OK.
;
ErExit: pop     de
       ret
;
; GetNxt -- get next date/time spec character for PrsDat and PrsTim
; On entry HL = address of next datespec position minus 1.  On exit
; HL incremented by 1 and A = character pointed to by HL.
;
GetNxt: inc     hl              ; next input
       ld      a,(hl)
       or      a               ; done?
       ret     nz              ; (no)
       pop     de              ; yes, remove return address
       jr      PsExit          ; ..and exit parse
;
; IsBcdD -- Test BCD date and time at HL for validity.  Returns NZ
; on error.  (Modified from ZSLIB's ZSISBCDD module by Carson Wilson.)
;
IsBcdD: push    hl
       ld      a,(hl)          ; BCD year
       call    IsBcd
       jr      nc,NotBcd
       inc     hl              ; month
       ld      a,(hl)
       or      a
       jr      z,NotBcd
       cp      13h
       call    c,IsBcd
       jr      nc,NotBcd
       inc     hl              ; day
       ld      a,(hl)
       or      a
       jr      z,NotBcd
       cp      32h
       call    c,IsBcd
       jr      nc,NotBcd
       inc     hl              ; hour
       ld      a,(hl)
       cp      24h
       call    c,IsBcd
       jr      nc,NotBcd
       inc     hl              ; min
       ld      a,(hl)
       cp      60h
       call    c,IsBcd
       jr      nc,NotBcd
       inc     hl              ; sec
       ld      a,(hl)
       cp      60h
       call    c,IsBcd
       jr      nc,NotBcd
       pop     hl
       xor     a               ; return Z no error
       ret

NotBcd: ld      a,(hl)          ; return NZ for error
       pop     hl
       or      0FFh
       ret
;
; IsBcd -- Test if byte in A is BCD.  Carry set (C) if byte is BCD.
;
IsBcd:  cp      09Ah            ; see if nibbles in 0..9
       ret     nc              ; not BCD if > 99
       and     00001111b       ; test right nibble
       cp      00Ah
       ret
;
; SetZST -- Set current date and time from DatTbl.  Returns A=0 and
; zero flag set (Z) if clock set, zero flag reset (NZ) on error.
; (Modified from ZSLIB's ZSGSTIME module by Carson Wilson.)
;
SetZST: ld      c,ZSSTime
       jr      GetSet
;
; GetZST -- Get current date and time to DatTbl.  Returns A=0 and zero
; flag set (Z) if buffer filled, zero flag reset (NZ) on error.
;
GetZST: ld      c,ZSGTime
;
GetSet: ld      de,DatTbl       ; point DE to buffer
       call    Bdos
       dec     a               ; ZSDOS returns 1 if okay
       ret
;
; IsDgt -- returns Zero Flag Set if char in A is numeric (0-9).
; Returns NZ if not.  Char in A is unaffected.  (Modified from
; SYSLIB 3.6 SISDIGIT module by Richard Conn.)
;
IsDgt:  push    bc              ; save BC
       ld      c,a             ; save character in C
       and     7Fh             ; mask out MSB
       cp      '0'             ; less than 0?
       jr      c,NoDgt
       cp      '9'+1           ; less than or equal to 9?
       jr      nc,NoDgt
       xor     a               ; set flag
       ld      a,c             ; get character
       pop     bc              ; restore BC
       ret
;
NoDgt:  ld      a,0FFh          ; set flag
       or      a
       ld      a,c             ; get character
       pop     bc              ; restore BC
       ret
;
; Eval16 -- Convert the string of ASCII hexadecimal digits pointed to
; by HL into a binary value; string is converted until invalid digit is
; encountered.  On return, HL points to error character, DE=value,
; A=E (low order 8 bits of value).  BC not affected.  (SYSLIB 3.6
; SEVAL2 module by Richard Conn.)
;
Eval16: push    bc              ; save BC
       ld      de,0            ; set DE = 0 initially
; Get next digit and check for '0' - '9'
E16L:   ld      a,(hl)          ; get byte
       call    Caps            ; capitalize
       cp      '0'             ; check for range
       jr      c,Done
       cp      'F'+1           ; check for range
       jr      nc,Done
       cp      '9'+1           ; check for 0-9
       jr      c,ProDec
       cp      'A'             ; check for out of range
       jr      c,Done
ProDec: sub     '0'             ; convert to binary
       cp      10
       jr      c,Proc
       sub     7               ; adjust for 'A'-'F'
; Proceed with processing
Proc:   push    af              ; save value
; Multiply DE by 16
Mul16:  push    hl              ; save HL
       ld      hl,0            ; Acc=0
       ld      b,16            ; 16 loops
Mul16L: add     hl,de           ; HL = HL + DE
       dec     b               ; count down
       jr      nz,Mul16L
       ld      d,h             ; new DE
       ld      e,l
       pop     hl              ; restore HL
; Add in A
       pop     af              ; get latest digit
       add     a,e             ; A = A + E
       ld      e,a
       ld      a,d             ; add to D if necessary
       adc     0
; Continue
       inc     hl              ; point to next character
       jr      E16L

;  Done -- result already in DE; set A = E
Done:   ld      a,e             ; A = E
       pop     bc              ; restore BC
       ret
;
; BcdBin -- packed BCD in A converted to binary in A.
;
BcdBin: push    bc
       ld      c,a             ; move value to C
       and     0F0h            ; mask lower nibble
       rra                     ; move upper nibble into lower
       rra
       rra
       rra
       ld      b,a             ; times 1
       add     a
       add     a               ; times 4
       add     b               ; times 5
       add     a               ; times 10
       ld      b,a             ; 10's digit to B
       ld      a,c             ; lower digit to A
       and     0Fh
       add     b               ; combine digits
       pop     bc
       ret
;
; Caps -- Capitalize ASCII Character in A.  (SYSLIB 3.6 SCAPS module
; by Richard Conn.)
;
Caps:   and     7Fh             ; mask out MSB
       cp      61h             ; less than lower-case a?
       ret     c
       cp      7Ah+1           ; between lower-case a and z?
       ret     nc
       and     5Fh             ; reset bit 5 to capitalize
       ret
;
; MvDat -- moves date or time from command line to storage
;
MvDat:  ld      c,SBLANK        ; any arguments?
       call    MEX
       ret     c               ; (if not, error)
       ld      hl,DatStr       ; point to string storage
       ld      b,8             ; eight characters maximum
       ld      c,GNC
MvDat1: push    hl
       push    bc
       call    MEX
       pop     bc
       pop     hl
       jr      c,MvDat2
       ld      (hl),a
       inc     hl
       djnz    MvDat1
MvDat2: xor     a               ; zero A and clear carry
       ld      (hl),a          ; insert a final null
       ret
;
; SILP -- In-line print routine (calls MEX)
;
SILP:   ld      c,ILP
       jp      MEX
;
; DatTbl -- BCD time and date storage
;
DatTbl:
Year:   db      0               ; 00 - 99
Month:  db      0               ;  1 - 12
Day:    db      0               ;  1 - 31
Hours:  db      0               ; 00 - 23
Minute: db      0               ; 00 - 59
Second: db      0               ; 00 - 59
;
DatStr: ds      9               ; command line date/time storage
;
       end