; RESIDENT TIME CLOCK DISPLAY with optional chime
;
;       Original author unidentified
;
;       Revised by Thomas A. Lundin
;               Graphics Unlimited Inc.
;               3000 Second St. No.
;               Minneapolis, MN 55411
;               (612) 588-7571
;
;       Later revisions -- 1/22/91
;
; usage: clok [/c]
;               /c to toggle chiming. No chimes by default.
;
; Clock display can be toggled on and off by repeating 'clok'.
;
;       Chimes should be toggled off when using heavy interrupt-driven
;       software such as communications to avoid losing characters or
;       hanging the system.
;
; 9/4/87 note:
;       This version uses the clock tick to regulate the duration of the
;       chimes, meaning that the chimes should be the same length from
;       one system to another, no matter what the CPU speed.
;       Also modified start-up routines to automatically set the time
;       display background to color or monochrome.
;
; 9/21/87 note:
;       This version alterates a date display with the time display, every
;       1.5 seconds.  The date display is static, i.e., it is not updated
;       at midnight (it would be inefficient to check every hour for an
;       event which occurs once every 24 hours).
;
;       Turning the clok off and on (typing clok, twice) will reset the date.
;
; 1/05/88 note:
;       This revision sets the clock display in the lower right corner
;       instead of the upper right.
;
; 1/22/91 note:
;      This revision causes the clock to automatically position itself
;      according to the number of columns in the display.  The program did
;      this before, but only when it was initially loaded.  Thus if you
;      switched between an 80 and 132 column mode, the clock would no longer
;      be displayed in the upper right hand corner or wherever.  The program
;      now reads the number of columns every time the clock is displayed.

; 2/1/91  The chime has now been replaced by a ®cuckooŻ.  The clock will now
;         cuckoo for the appropriate hourly figure and cuckoo once on the
;         quarter hour.

;v1.1   Toad Hall Tweak, 17 Apr 93
; - Retabified, reformatted for consistent upper, lower case.
; - Minor tweaks, tightening.
; David Kirschbaum, Toad Hall
; kirschd@sesi.com

TICKS   EQU     27              ; number of ticks between updates (1.5 sec)
                                ;old value -- 27
BEEPS1  EQU     1               ; duration of first beep in clock ticks
BEEPS2  EQU     3               ; duration of second beep in clock ticks
BEEPS3  EQU     1               ; pause between cuckoos
TONE1   EQU     5e7h            ; first chime tone 5f7h
TONE2   EQU     74ch            ; second chime tone

CR      EQU     0DH             ;v1.1
LF      EQU     0AH             ;v1.1

INTERRUPTS segment at 0h
        org     1ch*4           ; This is to use INT 1Ch
timer_int label dword           ; which is the timer interupt
INTERRUPTS ends

SCREEN  SEGMENT AT 0B000H       ; A dummy segment to use
SCREEN  ENDS                    ; as the Extra Segment

CSEG    SEGMENT
        ASSUME  CS:CSEG
        ORG     100h            ; org = 100 to make this into
                                ; a .COM file
First:  jmp     Load_Clock

old_time_int    dd      ?               ; The address INT 1Ch normally uses
count500        dw      TICKS           ; Used to update clock every nnn counts
beepticks       dw      0               ; number of ticks for a BEEP
beepsleft       db      0               ; number of beeps to output
cuckoo          dw      0               ; number of cuckoos to output
cursor          dw      0               ; Location of the cursor on the screen
beept           db      0               ; have-we-beeped flag
inbeep          db      0               ; beep-in-progress flag
flash           db      1               ; flashing colon flag
spkrstat        db      0               ; old speaker status
video_port      dw      ?               ; Video status port - check for scanning
numcol          db      ?               ; number of columns

hh              dw      0               ; hours
mm              dw      0               ; minutes
sc              dw      0               ; seconds
hh1             dw      0               ; 12-hour value for cuckoo
hhdiv           dw      32771           ; hours divisor (65543/2)
mmdiv           dw      546             ; minutes divisor (1092/2)
ssdiv           dw      9               ; second divisor (18/2)

displ           dw      (7020h)         ; leading space
                dw      5 dup(703ah)    ; Initial value for the clock
                dw      2 dup(7020h)    ; Add 2 ' 's for am/pm.

day_of_wk       dw      0               ; day of the week
month           dw      0               ; month
day             dw      0               ; day
chimon          dw      1               ; flag for chime in use or not
clokon          dw      1               ; flag for clok in use or not

Clock   PROC    NEAR                    ; The timer INT will now come here

        cmp     CS:clokon,1             ; is this interrupt silent ?
        jz      NewInt                  ; no, go execute it
         jmp    dword ptr old_time_int  ; silent, just execute old int
NewInt:
        pushf                   ; First call old time interrupt to update count
        call    dword ptr old_time_int

        call    NeedBeep                ; need to continue beep ?
        dec     CS:count500             ; should recalculate the time ?
        jnz     Dont_Recalculate

        push    ax                      ; Save the registers - good form
        push    cx
        push    di
        push    si
        push    ES

        xor     CS:flash,1              ; toggle the flashing colon
        call    Calc                    ; Recalculate the time
        mov     CS:count500,TICKS       ; Reset Count500
;
;Dont_Recalculate:
;This is the routine for recalculating clock position when you switch in and
;out of different column modes
        mov     ax,0            ;move memory location for column numbers
        mov     ES,ax           ;into ES:di
        mov     ax,044ah        ;
        mov     di,ax
        mov     ah,ES:[di]      ;move number of columns into ah
        sub     ah,8            ; Move to eight places before edge
        shl     ah,1            ; Mult by two (char and attribute bytes)

;end of clock position routine
        assume  ES:SCREEN       ; Set up screen as the Extra Segment
        mov     cx,SCREEN
        mov     ES,cx
        mov     dx,video_port           ; This is the screen status port
        mov     byte ptr cursor,ah      ; Move cursor to it's memory location

        mov     di,cursor       ; Set up cursor on screen as destination
;v1.1   lea     si,displ        ; Set up the display in memory as source
        mov     si,offset displ ;v1.1
        mov     cx,16           ; To move char and attributes
Scan_Low:
;v1.1 taking out the snow avoidance routines for CGA
;Scan_Low:                      ; Start waiting for a new horizontal scan
;       in      al,dx           ; i.e. the the vidio controller scan
;       test    al,1            ; status is low.
;       jnz     Scan_Low

        mov     ah,CS:[si]      ; Move byte to be written into AH

;Scan_High:                     ; After port has gone low, it must go high
;       in      al,dx           ; before it is safe to write directly
;       test    al,1            ; to the screen buffer in memory
;       jz      Scan_High

        mov     ES:[di],ah      ; Move to screen one byte at a time.
        inc     di              ; Position to attribute byte
        inc     si              ; on screen.

        loop    Scan_Low        ; Go back foe next byte

        pop     ES              ; Here are required pops to exit
        pop     si
        pop     di
        pop     cx
        pop     ax

Dont_Recalculate:
        iret                    ; An interrupt needs an IRET

Clock   ENDP

Calc    PROC    NEAR            ; Here we recalculate the time and store it
        push    ax              ; Puushes to save everytheing that
        push    bx              ; gets destroyed
        push    cx
        push    dx

        cmp     CS:flash,1              ; do date or time?
        jz      Dtime                   ; TIME
                                        ; DATE

;v1.1   lea     bx,displ        ; Set up BX as pointer to display in memory
        mov     bx,offset displ         ;v1.1
        mov     ax,CS:month
        mov     CS:[bx+0],ah            ; Move first month digit into display
        mov     CS:[bx+2],al            ; Then the second digit
        mov     byte ptr CS:[bx+4],'-'  ; a hyphen
        mov     ax,CS:day               ; get day
        mov     CS:[bx+6],ah            ; and move them into the display in memory
        mov     CS:[bx+8],al
        mov     byte ptr CS:[bx+10],' ' ; move space into display
        mov     ax,CS:day_of_wk
        mov     CS:[bx+12],ah           ; move day of the week into display
        mov     CS:[bx+14],al
        jmp     Restore

Dtime:
; note: Peter Norton p.223 explains that the time formula is more precisely
; shown as:
;       hh = clkcount / 65543
;       mm = hh.remainder / 1092
;       ss = mm.remainder / 18
;
; trouble is, the 65543 value won't work as a single-word divisor,
; so our trick is to divide the clock count and divisor values in half,
; which should have no appreciable affect on the accuracy of the time.

        xor     ax,ax           ; Set up for clock read.
        INT     1Ah             ; Read the clock.
        mov     bx,dx           ; Save low(minutes) portion.
        mov     dx,cx           ; Move high(hours) portion to AX.
        mov     ax,bx           ; dx:ax = clock count

        clc
        rcr     dx,1            ; div count by 2 so we can use a
        rcr     ax,1            ; single precision dividend

        div     CS:hhdiv        ; compute hours
        mov     CS:hh,ax        ; save it
        mov     ax,dx           ; prepare remainder for minutes
        xor     dx,dx
        div     CS:mmdiv        ; compute minutes

        cmp     ax,60           ; 60 minutes shows up sometimes
        jl      Mm_Ok           ; mostly it doesn't
         xor    ax,ax           ; but if it does, zero out the minutes
         inc    CS:hh           ; and bump up the hour

Mm_Ok:  mov     CS:mm,ax        ; save it
;       mov     ax,dx           ; prepare remainder for seconds
;       xor     dx,dx
;       div     CS:ssdiv        ; compute seconds
;       mov     CS:sc,ax        ; save it

;v1.1   lea     bx,displ        ; Set up BX as pointer to display in memory
        mov     bx,offset displ ;v1.1

        mov     byte ptr CS:[bx],' '    ; blank out first and last positions
        mov     byte ptr CS:[bx+14],' '
        mov     ax,CS:hh
        cmp     ax,12                   ; is it am or pm?
        jl      Am                      ; am
;Pm:
        mov     byte ptr CS:[bx+12],'p' ; Otherwise move 'P' into the display.
        sub     ax,12                   ; pm, subtract 12
        jmp     short Chek12            ; Continue.

Am:     mov     byte ptr CS:[bx+12],'a' ; Move an 'A' into the display.

Chek12: or      ax,ax                   ; Make zero hour...
        jnz     Am_Pm_Done
         mov    ax,12                   ; ...a twelve
Am_Pm_Done:
        mov     CS:hh1,ax               ; hour value for cuckoo
        aam                             ; Convert AX to BCD - a nice command
        add     ax,3030h                ; Add '0' to both bytes in AX to make ASCII
        cmp     ah,'0'                  ; Is the 1st digit '0'?
        jne     Dont_Edit               ; Then don't blank the character.
         mov    ah,' '                  ; Otherwise, put a space in AH.
Dont_Edit:
        mov     CS:[bx+2],ah            ; Move first hours digit into display
        mov     CS:[bx+4],al            ; Then the second digit
;----------------------------------
        mov     byte ptr CS:[bx+6],':'  ; in which case use a colon
        mov     ax,CS:mm                ; get minutes
        aam                             ; Again convert AX to Binary Coded Decimal
        add     ax,3030h                ; Add to make two ASCII characters
        mov     CS:[bx+8],ah            ; and move them into the display in memory
        mov     CS:[bx+10],al

;---------routine for alarm chime goes here------------------------------------
        cmp     CS:chimon,0             ; chimes off?
        jz      Restore                 ; yes, don't beep
        cmp     CS:inbeep,1             ; already in a beep loop?
        jz      Restore                 ; yes, don't be redundant

        cmp     ax,3030h                ; on the hour (full cuckoo routine)
        jz      Alarm3
        cmp     ax,3135h                ; on the 1/4 hour (single cuckoo)
        jz      Alarm2
        cmp     ax,3330h                ; on the 1/2 hour (single cuckoo)
        jz      Alarm2
        cmp     ax,3435h                ; on the 3/4 hour (single cuckoo)
        jz      Alarm2
        mov     CS:beept,0              ; we have not beeped
;------------------------------------------------------------------------------
Restore:                                ; Restore registers
        pop     dx
        pop     cx
ImRet2: pop     bx
ImRet1: pop     ax

ImRet:  ret
;-----------------------------------------------------------------------------
NeedBeep:
        cmp     CS:inbeep,1             ; are we beeping right now ?
        jnz     ImRet                   ; no, immediate return
        dec     CS:beepticks            ; yes, done beeping?
        jnz     ImRet                   ; no, immediate return
        push    ax
        mov     al,CS:spkrstat          ; yes, shut off speaker
        out     61h,al
        dec     CS:beepsleft            ; any more beeps waiting?
        jz      NoBeeps                 ; no, go home
        cmp     CS:beepsleft,1          ; how many await?
        jnz     Onward
         call   Pause
         jmp    ImRet1

Onward:
        push    bx
        mov     bx,TONE2                ; second tone in cuckoo
        mov     CS:beepticks,BEEPS2     ; second tone is longer than first tone
        call    Tone_a                  ; start it beeping
        jmp     ImRet2                  ; go home
NoBeeps:
        dec     CS:cuckoo               ; are there any more cuckoos?
        jz      No_Cuckoo               ; no more cuckoos
        push    bx
        mov     bx,TONE1                ; beginning of next cuckoo
        mov     CS:beepsleft,3
        call    Tone                    ;start it beeping
        jmp     ImRet2
;------------------------------------------------------------------------------
Alarm3: push    ax                      ;save the register
        mov     ax,hh1                  ;move hour value into ax
        mov     CS:cuckoo,ax            ; hourly cuckoo - repeat for number of hours
        pop     ax
        mov     bx,TONE1                ; send tone 1
        mov     CS:beepsleft,3
        call    Tone
        jmp     Restore

Alarm2: mov     CS:cuckoo,1             ; only a single cuckoo for quarter hour
                                        ; intervals
        mov     bx,TONE1                ; send tone 2
        mov     CS:beepsleft,2
        call    Tone
        jmp     Restore
;---------------------------------------
No_Cuckoo:
        mov     CS:beept,1              ; we have beeped
        mov     CS:inbeep,0             ; and we're not in one any more
        jmp     ImRet1
;---------------------------------------
Tone:
        cmp     CS:beept,1              ; do nothing if chime has been beeped
        jz      NoTone                  ; earlier in this clock update
        mov     CS:beepticks,BEEPS1     ; this long on beeps
Tone_a:
        MOV     AL,0B6H                 ; else condition the timer
        OUT     43H,AL
        MOV     AX,BX                   ; this is the freq
        OUT     42H,AL
        MOV     AL,AH
        OUT     42H,AL                  ; out you all go
        IN      AL,61H                  ; read spkr port
        MOV     CS:spkrstat,AL
        OR      AL,3
        OUT     61H,AL                  ; send a beep
        mov     CS:inbeep,1
NoTone: ret
;--------------------------------------------------------
Pause:
        mov     CS:beepticks,BEEPS3
        ret
;------------------------------------------------------------------------------
Calc    ENDP


Load_Clock PROC NEAR            ; This procedure initializes everything
        assume  DS:INTERRUPTS   ; The Data Segment will be the interrupt area
;       mov     ax,INTERRUPTS
        xor     ax,ax           ;0                              v1.1
        mov     DS,ax

        MOV     SI,0081H        ; addr of command line arguments
Next:   MOV     AL,CS:[SI]      ; get command line char
        CMP     AL,0DH          ; Return ends it.
        JZ      Again
        CMP     AL,'/'          ; switch char
        JZ      GetSwitch       ; see what it is
Next1:  INC     SI              ; else point to next char
        JMP     Next            ; and loop

GetSwitch:
        inc     si
        mov     al,CS:[si]
        cmp     al,'c'          ; chime toggle switch
;v1.1   jz      Sw_C
;        jmp    Next1           ; wrong switch
;
;Sw_C:
        jnz     Next1           ; wrong switch                          v1.1

        mov     CS:togchim,1    ; toggle them chimes
        jmp     Next1           ; get next switch if there is one

Again:  mov     ax,CS:sig_vector        ; get signature vector
        cmp     ax,5fh          ; if less than 0x60
        jg      Vok
         jmp    NoLoad          ; forget it

Vok:    add     ax,3500h
        int     21h
        mov     ax,ES
        cmp     ax,434ch                ; are we already loaded ?
        jnz     NoSig                   ; no
        cmp     bx,4f4bh
        jnz     NoSig                   ; and no
        mov     bx,word ptr timer_int   ; yes
        mov     ES,word ptr timer_int+2
        call    Gdate                   ; get the system date
        cmp     CS:togchim,1
        jnz     No1                     ; no toggle chimes
         call   ToglChim                ; go toggle chimes
         jmp    short Exit

No1:
        mov     ax,ES:[bx-2]
        xor     ax,1                    ; toggle activation mode
        mov     word ptr ES:[bx-2],ax
Exit:   mov     ax,4c00h                ; return to DOS
        int     21h

;---------------------------------------
ToglChim:
        mov     ax,ES:[bx-4]            ; get the chime flag
        xor     ax,1                    ; toggle it
        mov     word ptr ES:[bx-4],ax
        push    DS
        mov     di,CS
        mov     DS,di
;v1.1   cmp     ax,1                    ; beep if it's been turned on
;       jz      BeepMsg
;        mov    dx,offset chimoffmsg    ; chimes off
;        jmp    short Print
;BeepMsg:
;       mov     dx,offset chimonmsg     ; chimes on
;Print:
;v1.1   New code
        mov     dx,offset chimonmsg     ; chimes on
        cmp     ax,1                    ; beep if it's been turned on
        jz      BeepMsg
         mov    dx,offset chimoffmsg    ; chimes off
BeepMsg:
        mov     ah,9
        int     21h
        pop     DS
        ret
;---------------------------------------
NoSig:  mov     ax,ES                   ; no current signature...
        or      ax,bx                   ; ...but is it safe to load one?
        jz      SetSig                  ; yes
        mov     ax,CS:sig_vector        ; no, try another slot
        dec     ax
        mov     CS:sig_vector,ax
        jmp     Again
;---------------------------------------
Gdate:
        push    bx
        mov     ah,2ah          ; call DOS GetDate command
        int     21h
        xor     ah,ah           ; make an offset out of the day of the week
        clc
        rcl     ax,1
        mov     si,ax
        lea     bp,days
        mov     ax,[bp+si]      ; get the ASCII day of the week
        pop     bx
        mov     word ptr ES:[bx-10],ax  ; save it
        mov     al,dh                   ; month
        xor     ah,ah
        aam                     ; Convert AX to BCD - a nice command
        add     ax,3030h        ; Add '0' to both bytes in AX to make ASCII
        cmp     ah,'0'          ; Is the 1st digit '0'?
        jne     Gd1             ; Then don't blank the character.
        mov     ah,' '          ; Otherwise, put a space in AH.
Gd1:    mov     word ptr ES:[bx-8],ax
        mov     al,dl           ; day
        xor     ah,ah
        aam                     ; Convert AX to BCD - a nice command
        add     ax,3030h        ; Add '0' to both bytes in AX to make ASCII
        mov     word ptr ES:[bx-6],ax
        ret                     ; go home
;---------------------------------------
NoLoad  label near
        mov     ax,4c01h        ; abort with error
        int     21h

SetSig:
        cli
        push    DS
        mov     ax,CS:sig_vector
        add     ax,2500h        ; signature reads:
        mov     dx,434ch        ; 'CL'
        mov     DS,dx
        mov     dx,4f4bh        ; 'OK'
        int     21h
        pop     DS

        mov     ax,word ptr timer_int   ; Get the old interrupt service routine
        mov     word ptr old_time_int,ax        ; address and put it into our location
        mov     ax,word ptr timer_int+2 ; OLD_TIME_INT so we can still call it
        mov     word ptr old_time_int+2,ax

        mov     word ptr timer_int,offset Clock ; Now load the address of our clock
        mov     word ptr timer_int+2,CS ; routine into TIMER_INT so the timer
                                        ; interrupt will call CLOCK
        sti

        mov     ah,15           ; Ask for service 15 of INT 10H
        int     10h             ; This tells us how the display is set up
        sub     ah,8            ; Move to eight places before edge
        shl     ah,1            ; Mult by two (char and attribute bytes)
        mov     byte ptr cursor,ah ; Move cursor to it's memory location
;       mov     bx,3840 ;;;;;;;;;;;; *** move to lower right corner ***
;       add     cursor,bx ;;;;;;;;;; *** move to lower right corner ***
        mov     video_port,03bah        ; Assume this is a monochrome display
        test    al,4                    ; Is it?
        jnz     Get_Time                ; Yes - jump out
        add     cursor,8000h            ; No - set up for graphics display
        mov     video_port,03dah
        mov     cx,8
        mov     ax,4f20h
;v1.1   lea     di,displ
        mov     di,offset displ         ;v1.1
Color:  mov     CS:[di],ax
        inc     di
        inc     di
        loop    Color

Get_Time:
        mov     bx,offset Clock ; yes   ; get addr of clok
        mov     ax,CS
        mov     ES,ax
        cmp     CS:togchim,1    ; need to toggle ?
        jnz     Noct            ; no
         call   ToglChim
Noct:   call    Gdate

        mov     di,CS
        mov     DS,di
        mov     dx,offset hello         ; chimes on
        mov     ah,9
        int     21h
        call    Calc                    ; This is to avoid showing
                                        ; 00:00 for first 500 counts
        mov     dx,offset Load_Clock    ; Set up for everything but Load_Clock
        int     27h                     ; to stay attached to DOS

sig_vector      dw      67h             ; signature vector
togchim         db      0               ; to toggle chimes
days            db      'uSoMuTeWhTrFaS'
chimonmsg       db      'Chime ON',CR,LF,'$'
chimoffmsg      db      'Chime OFF',CR,LF,'$'
hello           db      'usage: clok [/c]  (/c: toggle chimes)',CR,LF
                db      'clok by itself toggles display',CR,LF
                db      'CLOK INSTALLED',CR,LF,'$'

Load_Clock      ENDP

        CSEG    ENDS

        END     First

