;-----------------------------------------------------------------------------
;                           ALARM.COM                                        :
;                                                                            :
;      THIS PROGRAM WILL READ A TIME OF DAY ( HH:MM ) FROM THE COMMAND LINE  :
;   BUFFER AND SOUND AN ALARM WHEN THE SYSTEM TIME REACHES THE ALARM TIME.   :
;   WHEN THE COMMAND 'ALARM hh:mm' IS ENTERED IN DOS, THIS PROGRAM WILL BE   :
;   LOADED AND EXECUTED.  IT FIRST INITIALIZES ITSELF BY READING THE TIME    :
;   FROM THE COMMAND BUFFER AND PLACES IT IN MEMORY, HOUR = 1 BYTE AND       :
;   MINUTE = 1 BYTE.  IT THEN SETS THE TIMER_INT INTERRUPT ( 1C HEX ) TO     :
;   POINT TO ITSELF.  IT THEN USES DOS FUNCTION 33 HEX TO TERMINATE AND STAY :
;   RESIDENT.                                                                :
;      AT EACH TIMER TICK ( 18.2 TIMES A SECOND ) CONTROL WILL JUMP TO THIS  :
;   PROGRAM.  EACH TIME THE PROGRAM IS INVOKED A COUNTER WILL BE INCREMENTED.:
;   WHEN THE COUNTER REACHES 546, 30 SECONDS HAVE ELAPSED AND THE MAIN PART  :
;   OF THE PROGRAM WILL BE EXECUTED, OTHERWISE A JUMP IS EXECUTED TO THE OLD :
;   INTERRUPT VECTOR.  THIS MEANS THAT THE ALARM WILL BE CHECKED ONLY ONCE   :
;   EVERY 30 SECONDS SO AS NOT TO SLOW DOWN THE SYSTEM EXCESSIVELY.          :
;      EVERY 30 SECONDS THE SYSTEM TIME IS COMPARED WITH THE ALARM TIME.  IF :
;   THE SYSTEM TIME IS LESS, WE JUMP TO THE OLD INTERRUPT VECTOR TO CONTINUE :
;   PROCESSING.  WHEN THE SYSTEM TIME IS EQUAL TO OR GREATER THAN THE ALARM  :
;   TIME THE ALARM SOUNDS.  THE PROGRAM THEN DISABLES ITSELF BY COPYING THE  :
;   JUMP TO OLD INTERRUPT VECTOR INSTRUCTION TO THE 'ALARM' ENTRY POINT.     :
;-----------------------------------------------------------------------------



; PSUEDO CODE ----------------------------------------------------------------
;
;        RESIDENT PART OF PROGRAM
;
;        JUMP TO INITIALIZATION PART OF PROGRAM
; START: INCREMENT INTERRUPT COUNTER
;        COMPARE TO 546 (THIRTY SECONDS?)
;        JUMP IF GREATER OR EQUAL TO 'CHECK TIME'
;        JUMP TO 'EXIT'
;
; CHECK: GET CURRENT TIME FROM BIOS (CH=HOURS, CL=MIN)
;        COMPARE WITH ALARM TIME
;        IF ALARM TIME <= CURRENT TIME, JUMP TO 'ALARM'
;        RESET INTERRUPT COUNTER
;        JUMP TO 'EXIT'
;
; ALARM: GENERATE ALARM
;        MOVE JMP INSTRUCTION TO 'START' (DISABLE THIS PROGRAM)

; EXIT:  JUMP TO OLD INTERRUPT VECTOR
;
;------- INITIALIZATION PART OF PROGRAM (REMOVED BY TSR) --------------------
;
; INIT:  GET ALARM HOURS:MINUTES FROM COMMAND LINE
;        CONVERT HOURS TO 24 HOUR FORMAT IF 'PM' ARGUMENT
;        STORE ALARM HOURS AND MINUTES
;        GET TIMER TICK INTERRUPT VECTOR (INT 1Ch) FROM DOS
;        STORE IN "OLDINT"
;        STORE OUR START ADDRESS IN INTERRUPT VECTOR
;        SET AVAILIBLE MEMORY ADDRESS TO "INIT"
;        TERMINATE AND STAY RESIDENT
;
;-----------------------------------------------------------------------------


CODE     SEGMENT PARA
         ASSUME CS:CODE, DS:CODE, ES:CODE
         ORG 100h

START:   JMP INIT                      ; JUMP TO INITIALIZATION ROUTINE


;------- DATA STORAGE --------------------------------------------------------

FREQ     = 546                         ; TIME CHECK FREQUENCY (30 SECS)

OLDINT   DD ?                          ; OLD INTERRUPT 1Ch VECTOR

COUNT    DW FREQ-1                     ; INTERRUPT COUNTER

MINUTE   DB 00                         ; ALARM MINUTE
HOUR     DB 00                         ; ALARM HOUR



;------- RESIDENT PART OF PROGRAM --------------------------------------------

NEW_INT  PROC     FAR
         ASSUME DS:NOTHING, ES:NOTHING

BEGIN:   STI                           ; ENABLE INTERUPTS
         PUSHF                         ; SAVE FLAGS ON STACK
         PUSH   AX                     ; SAVE REGISTERS
         PUSH   BX
         PUSH   CX
         PUSH   DX
         PUSH   DI
         PUSH   SI
         PUSH   DS
         PUSH   ES

         PUSH   CS                     ; RETRIEVE DATA SEGMENT
         POP    DS
         INC    COUNT                  ; INCREMENT INTERRUPT COUNTER
         CMP    COUNT,FREQ             ; TIME TO CHECK CLOCK?
         JB     EXIT                   ; EXIT IF NOT

;        CHECK TIME

CHECK:   MOV    AH,02h                 ; GET TIME FROM BIOS
         INT    1Ah                    ;   CH=HOURS, CL=MINUTES (BCD)
         CMP    CH,HOUR                ; COMPARE WITH ALARM HOUR
         JNE    RESET                  ; RESET IF CURRENT # ALARM HOUR
         CMP    CL,MINUTE              ; HOURS ARE SAME, COMPARE MINUTES
         JE     ALARM                  ; ALARM IF CURRENT = ALARM MINUTE
                                       ; OTHERWISE RESET COUNTER

RESET:   MOV    COUNT,0000             ; RESET INTERRUPT COUNTER
         JMP    EXIT                   ; JUMP TO EXIT


;------- GENERATE ALARM ------------------------------------------------------

ALARM:   MOV    DI,400                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,20                  ;
         CALL   BEEP                   ; MAKE THE TONE

         MOV    DI,500                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,20                  ;
         CALL   BEEP                   ; MAKE THE TONE

         MOV    DI,600                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,20                  ;
         CALL   BEEP                   ; MAKE THE TONE

         MOV    DI,700                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,10                  ;
         CALL   BEEP                   ; MAKE THE TONE

         MOV    AX,2                   ; PAUSE
OUTER:   MOV    CX,0FFFFh              ; OUTER LOOP
INNER:   LOOP   INNER                  ; INNER LOOP
         DEC    AX
         JA     OUTER

         MOV    DI,600                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,20                  ;
         CALL   BEEP                   ; MAKE THE TONE

         MOV    DI,700                 ; SET UP REGISTERS FOR BEEP PROCEDURE
         MOV    BX,40                  ;
         CALL   BEEP                   ; MAKE THE TONE


;        MOVE JMP INSTRUCTION TO ENTRY POINT (DISABLE THIS PROGRAM)

         PUSH   CS
         POP    ES
         MOV    SI,OFFSET JUMP         ; SET SEGMENT TO 0000
         MOV    DI,OFFSET NEW_INT
         MOV    CX,05
         CLD                           ; DIRECTION = FORWARD
         CLI                           ; DISABLE INTERRUPTS
         REP    MOVSB                  ; COPY JMP INSTRUCTION
         STI                           ; ALLOW INTERRUPTS AGAIN


;------- END THIS INTERRUPT ROUTINE ------------------------------------------

EXIT:    POP    ES
         POP    DS
         POP    SI
         POP    DI
         POP    DX
         POP    CX
         POP    BX
         POP    AX
         POPF

JUMP:    JMP    CS:OLDINT

NEW_INT  ENDP

;------- PROCEDURE TO CREATE A SINGLE TONE -----------------------------------
;        DI = FREQUENCY, BX = DURATION

BEEP     PROC    NEAR
         ASSUME  DS:NOTHING, ES:NOTHING

         PUSH    AX                    ; Save registers
         PUSH    BX
         PUSH    CX
         PUSH    DX
         PUSH    DI

         MOV     AL,0B6h               ; Write timer mode register
         OUT     43h,AL
         MOV     DX,14h                ; Timer divisor =
         MOV     AX,4F38h              ;   1331000/Frequency
         DIV     DI
         OUT     42h,AL                ; Write timer 2 count low byte
         MOV     AL,AH
         OUT     42h,AL                ; Write timer 2 count high byte
         IN      AL,61h                ; Get current Port B setting
         MOV     AH,AL                 ;   and save it in AH
         OR      AL,3                  ; Turn speaker on
         OUT     61H,AL
PAUSE:   MOV     CX,2801               ; Set duration counter
SPKRON:  LOOP    SPKRON                ;
         DEC     BX                    ; Speaker on count expired?
         JNZ     PAUSE                 ; If not, keep speaker on
         MOV     AL,AH                 ; Otherwise, restore port
         OUT     61h,AL
         POP     DI                    ; Restore registers
         POP     DX
         POP     CX
         POP     BX
         POP     AX
         RET                           ; And return
BEEP     ENDP



;------- INITIALIZATION PART OF PROGRAM (REMOVED BY TSR) ---------------------

         ASSUME CS:CODE, DS:CODE, ES:CODE

INIT:    CALL   PARSE                  ; PARSE COMMAND ARGUMENTS FOR TIME
         CMP    AL,0                   ; CHECK IF SYNTAX ERROR (AL=1)
         JE     GETVEC                 ; TERMINATE IF ERROR
         MOV    AH,4Ch                 ; DOS FUNCTION TO TERMINATE
         INT    21h                    ;


;        GET TIMER TICK INTERRUPT VECTOR (INT 1Ch) FROM DOS

GETVEC:  MOV    AX,351Ch               ; CALL DOS FUNCTION 35h
         INT    21h                    ;   TO GET OLD VECTOR
                                       ; STORE IN "OLDINT"
         MOV    WORD PTR [OLDINT],BX   ; VECTOR OFFSET  RETURNED IN BX
         MOV    WORD PTR [OLDINT+2],ES ; VECTOR SEGMENT RETURNED IN ES

         CALL   DISPTIME               ; DISPLAY TIME ALARM IS SET FOR


;        RESET INTERRUPT VECTOR TO POINT TO THIS PROGRAM

         MOV    DX,OFFSET NEW_INT      ; PUT OUR OFFSET IN DX
         MOV    AX,251Ch               ; CALL DOS FUNCTION 25 HEX TO
         INT    21h                    ;    RESET TIMER INTERRUPT VECTOR

;        TERMINATE BUT STAY RESIDENT USING DOS INTERRUPT 27h

         MOV    DX,OFFSET INIT         ; SET AVAILIBLE MEMORY ADDRESS TO "INIT"
         INT    27h                    ; CALL DOS INTERUPT 27 HEX TO TERMINATE




;------- PARSE COMMAND LINE TO GET ALARM TIME -------------------------------

PARSE    PROC   NEAR
         ASSUME CS:CODE, DS:CODE, ES:CODE

         CMP    BYTE PTR DS:80h,00     ; SEE IF ANY ARGUMENTS
         JA     PARAM                  ; IF NO, PROMPT FOR IT

         MOV    DX,OFFSET PROMPT       ; DISPLAY PROMPT
         MOV    AH,09h
         INT    21h

         MOV    DX,7Fh                 ; PUT START OF INPUT BUFFER IN DX
         MOV    BYTE PTR DS:7Fh,128    ; LENGTH OF INPUT BUFFER
         MOV    AH,0Ah                 ; CALL DOS TO GET INPUT
         INT    21h
         MOV    DL,10                  ; CALL DOS TO DISPLAY LINE FEED
         MOV    AH,02
         INT    21h

PARAM:   CALL   UPCASE                 ; CHANGE ARGUMENTS TO UPPER CASE
         MOV    SI,81h                 ; SET POINTER TO COMMAND ARGUMENTS
AGAIN1:  LODSB                         ; GET NEXT CHAR
         CMP    AL,' '                 ; CHECK FOR SPACE
         JE     AGAIN1                 ; IF YES, GET ANOTHER
         CMP    AL,'0'                 ; IS IT A NUMBER?
         JB     ERROR1                 ; IF NO, SYNTAX ERROR
         CMP    AL,'9'                 ;
         JA     ERROR1                 ;
         CALL   GETBCD                 ; GET HOUR IN BCD
         CMP    BL,23h                 ; MAKE SURE IT'S VALID (<24)
         JA     ERROR2                 ; DISPLAY MESSAGE IF NOT
         MOV    HOUR,BL                ; STORE HOUR

         CMP    AL,':'                 ; CHECK FOR COLON
         JNE    PM                     ; IF NOT CHECK FOR PM

         LODSB                         ; GET NEXT CHAR
         CMP    AL,'0'                 ; IS IT A NUMBER?
         JB     ERROR1                 ; IF NO, SYNTAX ERROR
         CMP    AL,'9'                 ;
         JA     ERROR1                 ;
         CALL   GETBCD                 ; GET MINUTE IN BCD
         CMP    BL,59h                 ; MAKE SURE IT'S VALID
         JA     ERROR3                 ; DISPLAY MESSAGE IF NOT
         MOV    MINUTE,BL              ; STORE MINUTE

PM:      CMP    AL,' '                 ; CHECK FOR SPACE
         JNE    P                      ; IF NOT CHECK FOR 'PM'
         LODSB                         ;   OTHERWISE GET NEXT CHAR
         JMP    PM                     ;   AND CHECK AGAIN
P:       CMP    AL,'P'                 ; CHECK FOR 'P'
         JNE    OK                     ; END IF NOT
         LODSB                         ; OTHERWISE GET NEXT CHAR
         CMP    AL,'M'                 ; CHECK FOR 'M'
         JNE    OK                     ; END IF NOT
                                       ; OTHERWISE CHANGE TO 'PM'
         MOV    AL,HOUR                ; LOAD HOUR
         CMP    AL,00                  ; CHECK IF HOUR = 0
         JE     OK                     ; END IF IT IS ( DON'T CHANGE IT )
         CMP    AL,11h                 ; CHECK IF HOUR < 12
         JA     OK                     ; END IF NOT
         ADD    AL,12h                 ; OTHERWISE ADD 12
         DAA                           ; ADJUST FOR BCD ADDITION
         MOV    HOUR,AL                ; STORE NEW 24 HOUR FORMAT

OK:      MOV    AL,00                  ; CLEAR ERROR FLAG
         JMP    ENDSUB1                ; ALL THROUGH

;        SYNTAX ERROR : NO OR INCORRECT ALARM TIME WAS ENTERED, PRINT MESSAGE
;        AND RETURN ERROR CODE (AL=1)

ERROR1:  MOV    DX,OFFSET MSG1         ; PUT MESSAGE 1 ADDRESS IN DX
         JMP    SHORT PRNT             ; JUMP TO PRINT
ERROR2:  MOV    DX,OFFSET MSG2         ; PUT MESSAGE 2 ADDRESS IN DX
         JMP    SHORT PRNT             ; JUMP TO PRINT
ERROR3:  MOV    DX,OFFSET MSG3         ; PUT MESSAGE 3 ADDRESS IN DX

PRNT:    MOV    AH, 09h                ; CALL DOS PRINT STRING FUNCTION
         INT    21h                    ;
         MOV    AL,01                  ; AL=1 : SYNTAX ERROR

ENDSUB1: RET                           ; RETURN TO MAIN PROGRAM

PARSE    ENDP   ;------------------------------------------------------------



;------- CHANGE COMMAND LINE PARAMETERS TO UPPER CASE -----------------------

UPCASE   PROC   NEAR
         ASSUME CS:CODE, DS:CODE, ES:CODE

         MOV    CL,BYTE PTR CS:80h     ; LOAD LENGTH OF ARGUMENTS
         MOV    CH,00                  ;
         MOV    SI,0000                ; SET UP SOURCE INDEX

NEXT:    CMP    CX,SI                  ; IF SI=CX THEN ARGUMENT LIST HAS
         JE     ENDSUB2                ;    BEEN PROCESSED
         INC    SI                     ; INCREMENT INDEX
         MOV    AL,BYTE PTR [80h+SI]   ; LOAD NEXT CHAR
         CMP    AL,'a'                 ; IS IT A LOWER CASE LETTER
         JB     NEXT                   ; GET NEXT IF NOT
         CMP    AL,'z'                 ;
         JA     NEXT                   ;
         SUB    AL,20h                 ; CHANGE TO UPPER CASE
         MOV    BYTE PTR [80h+SI],AL   ; STORE NEW CHAR
         JMP    NEXT                   ; GET NEXT CHAR

ENDSUB2: RET                           ; ALL DONE, RETURN

UPCASE   ENDP   ;------------------------------------------------------------



;------- PROCEDURE TO GET NUMBER FROM COMMAND LINE & CONVERT IT TO BCD ------

GETBCD   PROC   NEAR

         XOR    BX,BX                  ; CLEAR BX TO HOLD RESULT
         SUB    AL,30h                 ; ELSE, SUBTRACT 30H
         MOV    BL,AL                  ; STORE IT IN BL
         LODSB                         ; GET NEXT CHAR
         CMP    AL,30h                 ; IS IT A NUMBER?
         JB     ENDSUB3                ; IF NO, RETURN
         CMP    AL,39h                 ;
         JA     ENDSUB3                ;

         SUB    AL,30h                 ; ELSE, SUBTRACT 30H
         MOV    CL,04                  ; SHIFT FIRST NUMERAL LEFT 4
         SHL    BL,CL
         OR     BL,AL                  ; OR IN SECOND NUMERAL
         LODSB                         ; GET NEXT CHAR AND QUIT
ENDSUB3: RET                           ; RETURN

GETBCD   ENDP   ;------------------------------------------------------------



;------- DISPLAY ALARM SET MESSAGE ------------------------------------------

DISPTIME PROC   NEAR

         MOV    AL,HOUR                ; LOAD HOUR (BCD)
         MOV    AH,AL                  ; STORE IN AH
         CMP    AL,12h                 ; CHECK IF PM
         JB     AM                     ; LEAVE MESSAGE AT AM
         MOV    BYTE PTR SETMSG[34],112 ; CHANGE AM TO PM
         CMP    AL,13h                 ; CHECK IF HOUR NEEDS ADJUSTING
         JB     AM                     ; NOT IF < 13 (1 pm)
         SUB    AL,12h                 ; CHANGE TO 12 HOUR TIME
         DAS                           ; ADJUST FOR BCD SUBTRACTION
         MOV    AH,AL                  ; STORE RESULT IN AH
AM:      MOV    CL,4                   ; LOAD COUNTER
         SHR    AL,CL                  ; GET MOST SIGNIFICANT DIGIT
         CMP    AL,0                   ; LEAVE MSD BLANK IF ZERO
         JE     NOTENS                 ;
         ADD    AL,30h                 ; CHANGE TO ASCII
         MOV    BYTE PTR SETMSG[28],AL ; STORE MSD
NOTENS:  MOV    AL,AH                  ; GET ADJUSTED HOUR FROM AH
         AND    AL,0Fh                 ; MASK LEAST SIGNIFICANT DIGIT
         ADD    AL,30h                 ; CHANGE TO ASCII
         MOV    BYTE PTR SETMSG[29],AL ; STORE LSD
         MOV    AL,MINUTE              ; LOAD MINUTE (BCD)
         MOV    CL,4                   ; LOAD COUNTER
         SHR    AL,CL                  ; GET MSD
         ADD    AL,30h                 ; CHANGE TO ASCII
         MOV    BYTE PTR SETMSG[31],AL ; NO NEED TO CHECK FOR ZERO
         MOV    AL,MINUTE              ; RELOAD MINUTE
         AND    AL,0Fh                 ; MASK LSD
         ADD    AL,30h                 ; CHANGE TO ASCII
         MOV    BYTE PTR SETMSG[32],AL ; STORE LSD
         MOV    DX,OFFSET SETMSG       ; DISPLAY ALARM SET TIME
         MOV    AH, 09h                ; CALL DOS PRINT STRING FUNCTION
         INT    21h                    ;
         RET

DISPTIME ENDP   ;------------------------------------------------------------



MSG1:    DB 7,13,10,'The correct syntax is:  ALARM hh[:mm] [PM]',13,10,36
MSG2:    DB 7,13,10,'The hour can be 0 through 23',13,10,36
MSG3:    DB 7,13,10,'The minute can be 0 through 59',13,10,36
SETMSG:  DB   13,10,'An alarm has been set for  0:00 am',13,10,36
PROMPT:  DB   13,10,'Enter alarm time ( hh[:mm] [PM] ): ',36


CODE     ENDS
         END  START
