;
;=======================================================================
; PS/2 Keyboard/Mouse Information Utility (PS2INFO)
;=======================================================================
;
; Simple utility that performs simple tests of an 8242 PS/2 controller,
; keyboard, and mouse.
;
; WBW 2022-03-28: Add menu driven port selection
;                 Add support for Rhyophyre
; WBW 2022-04-01: Add menu for test functions
; WBW 2022-04-02: Fix prtchr register saving/recovery
; WBW 2023-10-19: Add support for Duodyne
; WBW 2024-06-10: Add support for RC2014
;
;=======================================================================
;
; PS/2 Keyboard/Mouse controller port addresses (adjust as needed)
;
; Nhyodyne:
iocmd_mbc	.equ	$E3	; PS/2 controller command port address
iodat_mbc	.equ	$E2	; PS/2 controller data port address
; Rhyophyre:
iocmd_rph	.equ	$8D	; PS/2 controller command port address
iodat_rph	.equ	$8C	; PS/2 controller data port address
; Duodyne:
iocmd_duo	.equ	$4D	; PS/2 controller command port address
iodat_duo	.equ	$4C	; PS/2 controller data port address
; RC2014 (EP/Sally)
iocmd_rc	.equ	$64	; PS/2 controller command port address
iodat_rc	.equ	$60	; PS/2 controller data port address

;
cpumhz	.equ	8	; for time delay calculations (not critical)
;
; General operational equates (should not requre adjustment)
;
stksiz	.equ	$40			; Working stack size
;
ltimout	.equ	0			; 256*10ms = 2.56s
stimout	.equ	10			; 10*10ms = 100ms
;
restart	.equ	$0000			; CP/M restart vector
bdos	.equ	$0005			; BDOS invocation vector
;
;=======================================================================
;
	.org	$100	; standard CP/M executable
;
;
	; setup stack (save old value)
	ld	(stksav),sp		; save stack
	ld	sp,stack		; set new stack
;
	call	crlf
	ld	de,str_banner		; banner
	call	prtstr
;
	call	setup
;
	call	main			; do the real work
;
exit:
	call	crlf2
	ld	de,str_exit
	call	prtstr

	; clean up and return to command processor
	call	crlf			; formatting
	ld	sp,(stksav)		; restore stack
	jp	restart			; return to CP/M via restart
;
;=======================================================================
; Select and setup for hardware
;=======================================================================
;
setup:
	call	crlf2
	ld	de,str_hwmenu
	call	prtstr
setup1:
	ld	c,$06			; BDOS direct console I/O
	ld	e,$FF			; Subfunction = read
	call	bdos
	cp	0
	jr	z,setup1
	call	upcase
	call	prtchr
	cp	'1'			; Nhyodyne
	jr	z,setup_mbc
	cp	'2'			; Rhyophyre
	jr	z,setup_rph
	cp	'3'			; Duodyne
	jr	z,setup_duo
	cp	'4'			; RC2014 EP/Sally
	jr	z,setup_rc
	cp	'X'
	jr	z,exit
	jr	setup
;
setup_mbc:
	ld	a,iocmd_mbc
	ld	(iocmd),a
	ld	a,iodat_mbc
	ld	(iodat),a
	ld	de,str_mbc
	jr	setup2
;
setup_rph:
	ld	a,iocmd_rph
	ld	(iocmd),a
	ld	a,iodat_rph
	ld	(iodat),a
	ld	de,str_rph
	jr	setup2
;
setup_duo:
	ld	a,iocmd_duo
	ld	(iocmd),a
	ld	a,iodat_duo
	ld	(iodat),a
	ld	de,str_duo
	jr	setup2
;
setup_rc:
	ld	a,iocmd_rc
	ld	(iocmd),a
	ld	a,iodat_rc
	ld	(iodat),a
	ld	de,str_rc
	jr	setup2
;
setup2:
	call	prtstr
	call	crlf2
	ld	de,str_cmdport
	call	prtstr
	;ld	a,iocmd
	ld	a,(iocmd)
	call	prthex
	call	crlf
	ld	de,str_dataport
	call	prtstr
	;ld	a,iodat
	ld	a,(iodat)
	call	prthex
;
	xor	a
	ret
;
;=======================================================================
; Main Program
;=======================================================================
;
main:
	call	crlf2
	ld	de,str_menu
	call	prtstr
main1:
	ld	c,$06			; BDOS direct console I/O
	ld	e,$FF			; Subfunction = read
	call	bdos
	cp	0
	jr	z,main1
	call	upcase
	call	prtchr
	cp	'X'
	jp	z,exit
	call	main2
	jr	main
;
main2:
	; Dispatch to test functions
	cp	'C'			; Test Controller
	jp	z,test_ctlr
	cp	'K'			; Test Keyboard
	jp	z,test_kbd
	cp	'M'			; Test Mouse
	jp	z,test_mse
	cp	'B'			; Test Both
	jp	z,test_kbdmse
	ret
;
; Test 8242 PS/2 Controller
;
test_ctlr:
	call	crlf2
	ld	de,str_ctlr
	call	prtstr
;
	call	ctlr_test
	ret	nz
;
	call	ctlr_test_p1
;
	call	ctlr_test_p2
;
	ret
;
; Test Keyboard
;
test_kbd:
;
; First, we attempt to contact the controller and keyboard, then
; print the keyboard identity and scan codes supported
;
	call	crlf2
	ld	de,str_basic
	call	prtstr
;
	call	ctlr_test
	jr	nz,test_kbd_fail
;
	ld	a,$20			; kbd enabled, mse disabled, no ints
	call	ctlr_setup
	jr	nz,test_kbd_fail
;
	call	ctlr_flush
;
	call	test_kbd_basic
	jr	nz,test_kbd_fail
;
; We make two passes through the test series with different controller
; setup values.  The first time is with scan code translation off and
; the second time with it on.
;
	; Run test series with translation off
	call	crlf2
	ld	de,str_trans_off
	call	prtstr
;
	ld	a,$20			; kbd enabled, xlat disabled, mouse disabled, no ints
	ld	(ctlr_cfgval),a
	call	test_kbd_keys
;
	; Run test series with translation on
	call	crlf2
	ld	de,str_trans_on
	call	prtstr
;
	ld	a,$60			; kbd enabled, xlat enabled, mouse disabled, no ints
	ld	(ctlr_cfgval),a
	call	test_kbd_keys
;
	ret
;
test_kbd_fail:
	ld	de,str_kbd_failed
	call	crlf2
	call	prtstr
	ret
;
; Test Mouse
;
test_mse:
	call	crlf2
	ld	de,str_basic_mse
	call	prtstr
;
	call	ctlr_test
	jr	nz,test_mse_fail
;
	ld	a,$10			; kbd disabled, mse enabled, no ints
	call	ctlr_setup
	jr	nz,test_mse_fail
;
	call	ctlr_flush
;
	call	mse_reset
	jr	nz,test_mse_fail
;
	call	ctlr_flush
;
	call	mse_ident
	jr	nz,test_mse_fail
;
	call	mse_stream
	jr	nz,test_mse_fail
;
	call	mse_echo
;	
	xor	a			; signal success
	ret
;
test_mse_fail:
	ld	de,str_mse_failed
	call	crlf2
	call	prtstr
	ret
;
; Test Everything
;
test_kbdmse:
	call	crlf2
	ld	de,str_kbdmse
	call	prtstr
;
	call	ctlr_test
	jr	nz,test_kbdmse_fail
;
	ld	a,$00			; kbd enabled, mse enabled, no ints
	call	ctlr_setup
	jr	nz,test_kbdmse_fail
;
	call	ctlr_flush
;
	call	kbd_reset
	jr	nz,test_kbdmse_fail
;
	call	ctlr_flush
;
	ld	a,2
	call	kbd_setsc
;
	call	mse_reset
	jr	nz,test_kbdmse_fail
;
	call	ctlr_flush
;
	call	mse_stream
	jr	nz,test_kbdmse_fail
;
	call	kbdmse_echo
;	
	xor	a			; signal success
	ret
;
test_kbdmse_fail:
	ld	de,str_kbdmse_failed
	call	crlf2
	call	prtstr
	ret
;
; Perform basic keyboard tests, display keyboard identity, and
; inventory the supported scan code sets.
;
test_kbd_basic:
	call	kbd_reset
	ret	nz
;
	call	ctlr_flush
;
	call	kbd_ident
	ret	nz
;
	ld	b,3			; Loop control, 3 scan code sets
	ld	c,1			; Current scan code number
test_kbd_basic1:
	ld	a,c			; Scan code set to A
	push	bc
	call	kbd_setsc		; Attempt to set it
	pop	bc
	push	af			; save result
	call	crlf2
	ld	de,str_sc_tag
	call	prtstr
	ld	a,c
	call	prtdecb
	pop	af			; restore result
	ld	de,str_sc_ok
	jr	z,test_kbd_basic2
	ld	de,str_sc_fail
test_kbd_basic2:
	call	prtstr
	inc	c
	djnz	test_kbd_basic1
;
	xor	a			; signal success
	ret
;
; This routine runs a series of controller and keyboard tests.  The
; desired controller setup value should be placed in ctlr_cfgval
; prior to invoking this routine.
;
test_kbd_keys:
	ld	a,(ctlr_cfgval)
	call	ctlr_setup
	ret	nz
;
	call	kbd_reset
	ret	nz
;
	call	kbd_ident
	;ret	nz
;
	ld	a,2
	call	kbd_setsc
	;ret	nz
;
	call	kbd_dispsc
	;ret	nz
;
	call	kbd_echo
	;ret	nz
;
	xor	a			; signal success
	ret
;
;=======================================================================
; Controller/Keyboard/Mouse Test Routines
;=======================================================================
;
; Attempt self-test command on PS/2 controller
;
;   PS/2 controller should respond with an 0x55 on data port
;   after being sent a 0xAA on the command port.
;
ctlr_test:
	call	crlf2
	ld	de,str_ctlr_test
	call	prtstr
	ld	a,$aa			; self-test command
	call	put_cmd_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$55			; expected value?
	jp	nz,err_ctlr_test	; handle self-test error
	call	crlf
	ld	de,str_ctlr_test_ok
	call	prtstr
	xor	a
	ret
;
; Attempt self-test of first port of controller
;
ctlr_test_p1:
	call	crlf2
	ld	de,str_ctlr_test_p1
	call	prtstr
	ld	a,$ab			; self-test first port
	call	put_cmd_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$00			; expected value?
	jp	nz,err_ctlr_test_p1	; handle self-test error
	call	crlf
	ld	de,str_ctlr_test_p1_ok
	call	prtstr
	xor	a
	ret
;
; Attempt self-test of second port of controller
;
ctlr_test_p2:
	call	crlf2
	ld	de,str_ctlr_test_p2
	call	prtstr
	ld	a,$a9			; self-test second port
	call	put_cmd_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$00			; expected value?
	jp	nz,err_ctlr_test_p2	; handle self-test error
	call	crlf
	ld	de,str_ctlr_test_p2_ok
	call	prtstr
	xor	a
	ret
;
; PS/2 controller setup
;
;   Set controller command register to value in A
;
ctlr_setup:
	push	af			; save incoming value
	call	crlf2
	ld	de,str_ctlr_setup
	call	prtstr
	ld	a,$60			; write to command register 0
	call	put_cmd_dbg
	pop	bc			; recover incoming to B
	jp	c,err_ctlr_to		; handle controller error
	ld	a,b
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	xor	a
	ret
;
; Flush incoming data buffer
;
ctlr_flush:
	call	crlf2
	ld	de,str_ctlr_flush
	call	prtstr
ctlr_flush1:
	call	delay			; small delay
	call	check_read		; data pending?
	ret	nz			; return if nothing there
	call	get_data_dbg		; get and discard byte
	jr	ctlr_flush1		; loop
;
; Perform a keyboard reset
;
kbd_reset:
	call	crlf2
	ld	de,str_kbd_reset
	call	prtstr
	ld	a,$ff			; Keyboard reset
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_reset
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$aa			; Success?
	jp	nz,err_kbd_reset
	call	crlf
	ld	de,str_kbd_reset_ok
	call	prtstr
	xor	a
	ret
;
; Identify keyboard
;
kbd_ident:
	call	crlf2
	ld	de,str_kbd_ident
	call	prtstr
	ld	a,$f2			; Identify keyboard command
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_ident
	; Now we need to receive 0-2 bytes.  There is no way to know
	; how many are coming, so we receive bytes until there is a
	; timeout error.  Timeout is shortened here so that we don't
	; have to wait seconds for the routine to complete normally.
	; A short timeout is more than sufficient here.
	ld	ix,workbuf
	ld	a,(timeout)		; save current timeout
	push	af
	ld	a,stimout		; set a short timeout
	ld	(timeout),a
	ld	b,8			; buf max
	ld	c,0			; buf len
kbd_ident1:
	push	bc
	call	get_data_dbg
	pop	bc
	jr	c,kbd_ident2
	ld	(ix),a
	inc	ix
	inc	c
	djnz	kbd_ident1
kbd_ident2:
	pop	af			; restore original timeout
	ld	(timeout),a
	call	crlf
	ld	de,str_kbd_ident_disp
	call	prtstr
	ld	a,'['
	call	prtchr
	ld	ix,workbuf
	ld	a,c			; bytes to print
	or	a			; check for zero
	jr	z,kbd_ident4		; handle zero
	ld	b,a			; setup loop counter
	jr	kbd_ident3a
kbd_ident3:
	ld	a,','
	call	prtchr
kbd_ident3a:
	ld	a,(ix)
	call	prthex
	inc	ix
	djnz	kbd_ident3
kbd_ident4:
	ld	a,']'
	call	prtchr
	xor	a
	ret
;
; Display keyboard active scan code set being used
;
kbd_dispsc:
	call	crlf2
	ld	de,str_kbd_getsc
	call	prtstr
	ld	a,$f0			; Keyboard get/set scan code
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_getsc
	ld	a,$00			; Get active scan code set
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_getsc
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	push	af
	call	crlf
	ld	de,str_kbd_dispsc
	call	prtstr
	pop	af
	call	prtdecb
	xor	a
	ret
;
; Set keyboard active scan code set to value in A
;
kbd_setsc:
	ld	(kbd_setsc_val),a	; Save incoming value
	call	crlf2
	ld	de,str_kbd_setsc
	call	prtstr
	call	prtdecb
	ld	a,$f0			; Keyboard get/set scan code
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_setsc
	ld	a,(kbd_setsc_val)	; Recover scan code set value
	call	put_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$FA			; Is it an ack as expected?
	jp	nz,err_kbd_setsc
	xor	a
	ret
;
kbd_setsc_val	.db	0
;
; Read and display raw scan codes
;
kbd_echo:
	call	crlf2
	ld	de,str_disp_scan_codes
	call	prtstr
read_loop:
	ld	c,$06			; BDOS direct console I/O
	ld	e,$FF			; Subfunction = read
	call	bdos
	cp	$1B			; Escape key?
	ret	z
	call	check_read_kbd
	jr	nz,read_loop
	call	get_data
	jp	c,err_ctlr_to		; handle controller error
	push	af
	ld	a,' '
	call	prtchr
	ld	a,'['
	call	prtchr
	pop	af
	call	prthex
	ld	a,']'
	call	prtchr
	jr	read_loop
;
; Reset Mouse
;
mse_reset:
	call	crlf2
	ld	de,str_mse_reset
	call	prtstr
	ld	a,$ff			; Identify mouse command
	call	put_data_mse_dbg
	jp	c,err_ctlr_to		; handle controller error
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$fa			; Is it an ack as expected?
	jp	nz,err_mse_reset
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error
	cp	$aa			; Success?
	jp	nz,err_mse_reset
	call	crlf
	ld	de,str_mse_reset_ok
	call	prtstr
	xor	a
	ret
;
; Identify Mouse
;
mse_ident:
	call	crlf2
	ld	de,str_mse_ident
	call	prtstr
	ld	a,$f2			; Identify mouse command
	call	put_data_mse_dbg
	jp	c,err_ctlr_to		; handle controller error
mse_ident0:
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error

	;cp	$00			; extraneous?
	;jr	z,mse_ident0		; ignore it, get another

	cp	$fa			; Is it an ack as expected?
	jp	nz,err_mse_ident
	; Now we need to receive 0-2 bytes.  There is no way to know
	; how many are coming, so we receive bytes until there is a
	; timeout error.  Timeout is shortened here so that we don't
	; have to wait seconds for the routine to complete normally.
	; A short timeout is more than sufficient here.
	ld	ix,workbuf
	ld	a,(timeout)		; save current timeout
	push	af
	ld	a,stimout		; set a short timeout
	ld	(timeout),a
	ld	b,8			; buf max
	ld	c,0			; buf len
mse_ident1:
	push	bc
	call	get_data_dbg
	pop	bc
	jr	c,mse_ident2
	ld	(ix),a
	inc	ix
	inc	c
	djnz	mse_ident1
mse_ident2:
	pop	af			; restore original timeout
	ld	(timeout),a
	call	crlf
	ld	de,str_mse_ident_disp
	call	prtstr
	ld	a,'['
	call	prtchr
	ld	ix,workbuf
	ld	a,c			; bytes to print
	or	a			; check for zero
	jr	z,mse_ident4		; handle zero
	ld	b,a			; setup loop counter
	jr	mse_ident3a
mse_ident3:
	ld	a,','
	call	prtchr
mse_ident3a:
	ld	a,(ix)
	call	prthex
	inc	ix
	djnz	mse_ident3
mse_ident4:
	ld	a,']'
	call	prtchr
	xor	a
	ret
;
; Enable mouse packet streaming
;
mse_stream:
	call	crlf2
	ld	de,str_mse_stream
	call	prtstr
	ld	a,$f4			; Stream packets cmd
	call	put_data_mse_dbg
	jp	c,err_ctlr_to		; handle controller error
mse_stream0:
	call	get_data_dbg
	jp	c,err_ctlr_to		; handle controller error

	;cp	$00			; extraneous?
	;jr	z,mse_stream0		; ignore it, get another

	cp	$FA			; Is it an ack as expected?
	jp	nz,err_mse_stream
	xor	a
	ret
;
; Read and display raw mouse packets
;
mse_echo:
	call	crlf2
	ld	de,str_disp_mse_pkts
	call	prtstr
	call	mse_track_disp		; show mouse status
	xor	a
	ld	(msebuflen),a
mse_echo1:
	ld	c,$06			; BDOS direct console I/O
	ld	e,$FF			; Subfunction = read
	call	bdos
	cp	$1B			; Escape key?
	ret	z
	call	check_read_mse
	jr	nz,mse_echo1
	call	get_data
	jp	c,err_ctlr_to		; handle controller error
	push	af
	ld	a,(msebuflen)		; current bytes in buf
	ld	hl,msebuf		; start of buf
	call	addhla			; point to next buf pos
	pop	af
	ld	(hl),a			; save byte in buf
	ld	a,(msebuflen)
	inc	a
	ld	(msebuflen),a		; inc buf len
	cp	3			; got 3 bytes?
	jr	nz,mse_echo1	; if not, get some more
	call	mse_track
	call	mse_track_disp
	jr	mse_echo1		; and loop
;
; Read and display data from keyboard and mouse
;
kbdmse_echo:
	call	crlf2
	ld	de,str_disp_kbdmse
	call	prtstr
	xor	a
	ld	(msebuflen),a
	call	kbdmse_track_disp
;
kbdmse_echo1:
	; Check for user abort
	ld	c,$06			; BDOS direct console I/O
	ld	e,$FF			; Subfunction = read
	call	bdos
	cp	$1B			; Escape key?
	ret	z
;	
	call	kbdmse_echo2
	call	kbdmse_echo3
	jr	kbdmse_echo1
;
kbdmse_echo2:
	; Check & handle keyboard data
	call	check_read_kbd
	ret	nz
	call	get_data
	ld	(kbd_byte),a
	call	kbdmse_track_disp
	ret
;
kbdmse_echo3:
	; Check & handle mouse data
	call	check_read_mse
	ret	nz
	call	get_data
	jp	c,err_ctlr_to		; handle controller error
	push	af
	ld	a,(msebuflen)		; current bytes in buf
	ld	hl,msebuf		; start of buf
	call	addhla			; point to next buf pos
	pop	af
	ld	(hl),a			; save byte in buf
	ld	a,(msebuflen)
	inc	a
	ld	(msebuflen),a		; inc buf len
	cp	3			; full packet?
	ret	nz			; if not, loop
	call	mse_track
	call	kbdmse_track_disp
	ret
;
; Update mouse tracking stuff
; This routine assumes that msebuf has been filled with a complete
; 3 byte mouse packet.
;
mse_track:
	; Buttons...
	ld	a,(msebuf)
	ld	(mse_stat),a
;
	; X Coordinate
	ld	a,(msebuf+1)
	ld	e,a
	ld	d,0
	ld	a,(msebuf)
	and	%00010000		; sign bit
	jr	z,mse_track_x
	ld	d,$ff			; sign extend
mse_track_x:
	ld	hl,(mse_x)
	add	hl,de
	ld	(mse_x),hl		; save result
;
	; Y Coordinate
	ld	a,(msebuf+2)
	ld	e,a
	ld	d,0
	ld	a,(msebuf)
	and	%00100000		; sign bit
	jr	z,mse_track_y
	ld	d,$ff			; sign extend
mse_track_y:
	ld	hl,(mse_y)
	add	hl,de
	ld	(mse_y),hl		; save result
;
	; Reset mouse buffer
	xor	a
	ld	(msebuflen),a
	ret
;
; Display current mouse tracking info (buttons and coordinates)
;
mse_track_disp:
	ld	a,13			; CR only
	call	prtchr
	ld	de,str_msestat1		; "L="
	call	prtstr
	ld	a,(mse_stat)
	and	%00000001
	call	updown
	ld	de,str_msestat2		; ", M="
	call	prtstr
	ld	a,(mse_stat)
	and	%00000100
	call	updown
	ld	de,str_msestat3		; ", R="
	call	prtstr
	ld	a,(mse_stat)
	and	%00000010
	call	updown
;
	ld	de,str_msestat4		; ", X="
	call	prtstr
	ld	hl,(mse_x)		; save result
	call	prthexword
;
	ld	de,str_msestat5		; ", Y="
	call	prtstr
	ld	hl,(mse_y)		; save result
	call	prthexword
;
	ret
;
updown:
	jr	nz,updown1
	ld	de,str_up
	jr	updown2
updown1:
	ld	de,str_down
updown2:
	call	prtstr
	ret
;
; Display all keyboard and mouse tracking
;
kbdmse_track_disp:
	call	mse_track_disp
	ld	a,' '
	call	prtchr
	ld	a,'['
	call	prtchr
	ld	a,(kbd_byte)
	call	prthex
	ld	a,']'
	call	prtchr
	ret
;
;=======================================================================
; PS/2 Controller I/O Routines
;=======================================================================
;
wait_write:
;
; Wait for controller to be ready for a write
;   A=0 indicates success (ZF set)
;
	ld	a,(timeout)		; setup timeout constant
	ld	b,a
wait_write1:
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	in	a,(c)			; get status
	ld	c,a			; save status
	and	$02			; isolate input buf status bit
	ret	z			; 0 means ready, all done
	call	delay			; wait a bit
	djnz	wait_write1		; loop until counter exhausted
;	ld	de,str_timeout_write	; write timeout message
;	call	crlf
;	call	prtstr
;	ld	a,c			; recover last status value
;	call	prthex
	or	$ff			; signal error
	ret
;
wait_read:
;
; Wait for controller to be ready to read a byte
;   A=0 indicates success (ZF set)
;
	ld	a,(timeout)		; setup timeout constant
	ld	b,a
wait_read1:
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	in	a,(c)			; get status
	ld	c,a			; save status
	and	$01			; isolate input buf status bit
	xor	$01			; invert so 0 means ready
	ret	z			; if 0, all done
	call	delay			; wait a bit
	djnz	wait_read1		; loop until counter exhausted
;	ld	de,str_timeout_read	; write timeout message
;	call	crlf
;	call	prtstr
;	ld	a,c			; recover last status value
;	call	prthex
	or	$ff			; signal error
	ret
;
check_read:
;
; Check for data ready to read
;   A=0 indicates data available (ZF set)
;
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	in	a,(c)			; get status
	and	$01			; isolate input buf status bit
	xor	$01			; invert so 0 means ready
	ret
;
check_read_kbd:
;
; Check for keyboard data ready to read
;   A=0 indicates data available (ZF set)
;
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	in	a,(c)			; get status
	and	%00100001		; isolate input buf status bit
	cp	%00000001		; data ready, not mouse
	ret
;
check_read_mse:
;
; Check for mouse data ready to read
;   A=0 indicates data available (ZF set)
;
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	in	a,(c)			; get status
	and	%00100001		; isolate input buf status bit
	cp	%00100001		; data ready, is mouse
	ret
;
put_cmd:
;
; Put a cmd byte from A to the controller with timeout
; CF set indicates timeout error
;
	ld	e,a			; save incoming value
	call	wait_write		; wait for controller ready
	jr	z,put_cmd1		; if ready, move on
	scf				; else, signal timeout error
	ret				; and bail out
put_cmd1:
	ld	a,(iocmd)		; cmd port
	ld	c,a			; ... to C
	ld	a,e			; recover value to write
	out	(c),a			; write it
	or	a			; clear CF for success
	ret
;
put_cmd_dbg:
	call	put_cmd
	ret	c
	push	af

	call	crlf
	ld	de,str_put_cmd
	call	prtstr
	call	prthex

	pop	af
	ret
;	
put_data:
;
; Put a data byte from A to the controller interface with timeout
; CF set indicates timeout error
;
	ld	e,a			; save incoming value
	call	wait_write		; wait for controller ready
	jr	z,put_data1		; if ready, move on
	scf				; else, signal timeout error
	ret				; and bail out
put_data1:
	ld	a,(iodat)		; data port
	ld	c,a			; ... to C
	ld	a,e			; recover value to write
	out	(c),a			; write it
	or	a			; clear CF for success
	ret
;
put_data_dbg:
	call	put_data
	ret	c
	push	af

	call	crlf
	ld	de,str_put_data
	call	prtstr
	call	prthex

	pop	af
	ret

;	
put_data_mse:
;
; Put a data byte from A to the mouse interface with timeout
; CF set indicates timeout error
;
	ld	e,a			; save incoming value
	push	de
	
	ld	a,$d4			; mouse channel prefix
	call	put_cmd
	pop	de
	ret	c
	
	ld	a,e			; recover value
	call	put_data
	ret
;
put_data_mse_dbg:
	ld	e,a			; save incoming value
	push	de
	
	ld	a,$d4			; mouse channel prefix
	call	put_cmd_dbg
	pop	de
	ret	c
	
	ld	a,e			; recover value
	call	put_data_dbg
	ret
;
; Get a data byte from the controller interface to A with timeout
; CF set indicates timeout error
;
get_data:
	call	wait_read		; wait for byte to be ready
	jr	z,get_data1		; if readym, move on
	scf				; else signal timeout error
	ret				; and bail out
get_data1:
	ld	a,(iodat)		; data port
	ld	c,a			; ... to C
	in	a,(c)			; get data byte
	or	a			; clear CF for success
	ret
;
get_data_dbg:
	call	get_data
	ret	c
	push	af

	call	crlf
	ld	de,str_get_data
	call	prtstr
	call	prthex

	pop	af
	ret
;
; Error Handlers
;
err_ctlr_to:
	ld	de,str_err_ctlr_to
	jr	err_ret
;
err_ctlr_test:
	ld	de,str_err_ctlr_test
	jr	err_ret
;
err_ctlr_test_p1:
	ld	de,str_err_ctlr_test_p1
	jr	err_ret
;
err_ctlr_test_p2:
	ld	de,str_err_ctlr_test_p2
	jr	err_ret
;
err_kbd_reset:
	ld	de,str_err_kbd_reset
	jr	err_ret
;
err_kbd_getsc:
	ld	de,str_err_kbd_getsc
	jr	err_ret
;
err_kbd_setsc:
	ld	de,str_err_kbd_setsc
	jr	err_ret
;
err_kbd_ident:
	ld	de,str_err_kbd_ident
	jr	err_ret
;
err_mse_reset:
	ld	de,str_err_mse_reset
	jr	err_ret
;
err_mse_ident:
	ld	de,str_err_mse_ident
	jr	err_ret
;
err_mse_stream:
	ld	de,str_err_mse_stream
	jr	err_ret
;
err_ret:
	call	crlf2
	call	prtstr
	or	$ff			; signal error
	ret
;
;=======================================================================
; Utility Routines
;=======================================================================
;
; Print character in A without destroying any registers
;
prtchr:
	push	af		; save registers
	push	bc
	push	de
	push	hl
	ld	e,a		; character to print in E
	ld	c,$02		; BDOS function to output a character
	call	bdos		; do it
	pop	hl		; restore registers
	pop	de
	pop	bc
	pop	af
	ret
;
prtdot:
;
	; shortcut to print a dot preserving all regs
	push	af		; save af
	ld	a,'.'		; load dot char
	call	prtchr		; print it
	pop	af		; restore af
	ret			; done
;
; Uppercase character in A
;
upcase:
	cp	'a'			; below 'a'?
	ret	c			; if so, nothing to do
	cp	'z'+1			; above 'z'?
	ret	nc			; if so, nothing to do
	and	~$20			; convert character to lower
	ret				; done
;
; Print a zero terminated string at (de) without destroying any registers
;
prtstr:
	push	af
	push	de
;
prtstr1:
	ld	a,(de)		; get next char
	or	a
	jr	z,prtstr2
	call	prtchr
	inc	de
	jr	prtstr1
;
prtstr2:
	pop	de		; restore registers
	pop	af
	ret
;
; Print a hex value prefix "0x"
;
prthexpre:
	push	af
	ld	a,'0'
	call	prtchr
	ld	a,'x'
	call	prtchr
	pop	af
	ret
;
; Print the value in A in hex without destroying any registers
;
prthex:
	call	prthexpre
prthex1:
	push	af		; save AF
	push	de		; save DE
	call	hexascii	; convert value in A to hex chars in DE
	ld	a,d		; get the high order hex char
	call	prtchr		; print it
	ld	a,e		; get the low order hex char
	call	prtchr		; print it
	pop	de		; restore DE
	pop	af		; restore AF
	ret			; done
;
; print the hex word value in hl
;
prthexword:
	call	prthexpre
prthexword1:
	push	af
	ld	a,h
	call	prthex1
	ld	a,l
	call	prthex1 
	pop	af
	ret
;
; print the hex dword value in de:hl
;
prthex32:
	call	prthexpre
	push	bc
	push	de
	pop	bc
	call	prthexword1
	push	hl
	pop	bc
	call	prthexword1
	pop	bc
	ret
;
; Convert binary value in A to ascii hex characters in DE
;
hexascii:
	ld	d,a		; save A in D
	call	hexconv		; convert low nibble of A to hex
	ld	e,a		; save it in E
	ld	a,d		; get original value back
	rlca			; rotate high order nibble to low bits
	rlca
	rlca
	rlca
	call	hexconv		; convert nibble
	ld	d,a		; save it in D
	ret			; done
;
; Convert low nibble of A to ascii hex
;
hexconv:
	and	$0F	     	; low nibble only
	add	a,$90
	daa	
	adc	a,$40
	daa	
	ret
;
; Print value of A or HL in decimal with leading zero suppression
; Use prtdecb for A or prtdecw for HL
;
prtdecb:
	push	hl
	ld	h,0
	ld	l,a
	call	prtdecw		; print it
	pop	hl
	ret
;
prtdecw:
	push	af
	push	bc
	push	de
	push	hl
	call	prtdec0
	pop	hl
	pop	de
	pop	bc
	pop	af
	ret
;
prtdec0:
	ld	e,'0'
	ld	bc,-10000
	call	prtdec1
	ld	bc,-1000
	call	prtdec1
	ld	bc,-100
	call	prtdec1
	ld	c,-10
	call	prtdec1
	ld	e,0
	ld	c,-1
prtdec1:
	ld	a,'0' - 1
prtdec2:
	inc	a
	add	hl,bc
	jr	c,prtdec2
	sbc	hl,bc
	cp	e
	ret	z
	ld	e,0
	call	prtchr
	ret
;
; Start a new line
;
crlf2:
	call	crlf		; two of them
crlf:
	push	af		; preserve AF
	ld	a,13		; <CR>
	call	prtchr		; print it
	ld	a,10		; <LF>
	call	prtchr		; print it
	pop	af		; restore AF
	ret
;
; Add hl,a
;
;   A register is destroyed!
;
addhla:
	add	a,l
	ld	l,a
	ret	nc
	inc	h
	ret
;
; Delay ~10ms
;
delay:
	push	af
	push	de
	ld	de,625			; 10000us/16us
delay0:
	ld	a,(cpuscl)
delay1:
	dec	a
	jr	nz,delay1
	dec	de
	ld	a,d
	or	e
	jp	nz,delay0
	pop	de
	pop	af
	ret
;
;
;
;=======================================================================
; Constants
;=======================================================================
;
str_banner		.db	"PS/2 Keyboard/Mouse Information v0.9, 10-Jun-2024",0
str_hwmenu		.db	"PS/2 Controller Port Options:\r\n\r\n"
			.db	"  1 - Nhyodyne\r\n"
			.db	"  2 - Rhyophyre\r\n"
			.db	"  3 - Duodyne\r\n"
			.db	"  4 - RC2014\r\n"
			.db	"  X - Exit Application\r\n"
			.db	"\r\nSelection? ",0
str_mbc			.db	"Nhyodyne",0
str_rph			.db	"Rhyophyre",0
str_duo			.db	"Duodyne",0
str_rc			.db	"RC2014 (Saly)",0
str_menu		.db	"PS/2 Testing Options:\r\n\r\n"
			.db	"  C - Test PS/2 Controller\r\n"
			.db	"  K - Test PS/2 Keyboard\r\n"
			.db	"  M - Test PS/2 Mouse\r\n"
			.db	"  B - Test Both PS/2 Keyboard and Mouse Together\r\n"
			.db	"  X - Exit Application\r\n"
			.db	"\r\nSelection? ",0
str_exit		.db	"Done, Thank you for using PS/2 Keyboard/Mouse Information!",0
str_cmdport		.db	"Controller Command Port: ",0
str_dataport		.db	"Controller Data Port: ",0
str_err_ctlr_to		.db	"Controller I/O Timeout",0
str_err_ctlr_test	.db	"Controller Self-Test Failed",0
str_put_cmd		.db	"  Sent Command ",0
str_put_data		.db	"  Sent Data ",0
str_get_data		.db	"  Got Data ",0
str_ctlr_test		.db	"Attempting Controller Self-Test",0
str_ctlr_test_ok	.db	"Controller Self-Test OK",0
str_ctlr_test_p1	.db	"Attempting Self-Test of First Controller Port",0
str_ctlr_test_p1_ok	.db	"Controller First Port Self-Test OK",0
str_err_ctlr_test_p1	.db	"Controller First Port Self-Test Failed",0
str_ctlr_test_p2	.db	"Attempting Self-Test of Second Controller Port",0
str_ctlr_test_p2_ok	.db	"Controller Second Port Self-Test OK",0
str_err_ctlr_test_p2	.db	"Controller Second Port Self-Test Failed",0
str_ctlr_setup		.db	"Performing Controller Setup",0
str_ctlr		.db	"***** Basic 8242 PS/2 Controller Tests *****",0
str_basic		.db	"***** Basic Keyboard Checks and Scan Code Inventory *****",0
str_trans_off		.db	"***** Testing Keyboard with Scan Code Translation DISABLED *****",0
str_trans_on		.db	"***** Testing Keyboard with Scan Code Translation ENABLED *****",0
str_basic_mse		.db	"***** Basic Mouse Tests *****",0
str_kbdmse			.db	"***** Test All Devices Combined *****",0
str_ctlr_flush		.db	"Flushing controller input buffer",0
str_kbd_reset		.db	"Attempting Keyboard Reset",0
str_kbd_reset_ok	.db	"Keyboard Reset OK",0
str_err_kbd_reset	.db	"Keyboard Reset Failed",0
str_mse_reset		.db	"Attempting Mouse Reset",0
str_mse_reset_ok	.db	"Mouse Reset OK",0
str_err_mse_reset	.db	"Mouse Reset Failed",0
str_kbd_getsc		.db	"Requesting Active Scan Code Set from Keyboard",0
str_kbd_dispsc		.db	"Active Keyboard Scan Code Set is #",0
str_err_kbd_getsc	.db	"Error getting Active Keyboard Scan Code Set",0
str_kbd_setsc		.db	"Setting Active Keyboard Scan Code Set to #",0
str_err_kbd_setsc	.db	"Error setting Active Keyboard Scan Code Set",0
str_kbd_ident		.db	"Keyboard Identification",0
str_kbd_ident_disp	.db	"Keyboard Identity: ",0
str_mse_ident		.db	"Mouse Identification",0
str_mse_ident_disp	.db	"Mouse Identity: ",0
str_mse_stream		.db	"Enable Mouse Packet Streaming",0
str_err_mse_stream	.db	"Error enabling Mouse Packet Streaming",0
str_msestat1		.db	"L=",0
str_msestat2		.db	", M=",0
str_msestat3		.db	", R=",0
str_msestat4		.db	", X=",0
str_msestat5		.db	", Y=",0
str_up			.db	"UP",0
str_down		.db	"DN",0
str_sc_tag		.db	"Scan Code Set #",0
str_sc_ok		.db	" IS supported",0
str_sc_fail		.db	" IS NOT supported",0
str_err_kbd_ident	.db	"Error performing Keyboard Identification",0
str_err_mse_ident	.db	"Error performing Mouse Identification",0
str_disp_scan_codes	.db	"Displaying Raw Scan Codes",13,10
			.db	"  Press keys on test keyboard to display scan codes",13,10
			.db	"  Press <esc> on CP/M console to end",13,10,13,10,0
str_disp_mse_pkts	.db	"Displaying Mouse Packets",13,10
			.db	"  Move mouse and click mouse buttons",13,10
			.db	"  Press <esc> on CP/M console to end",13,10,13,10,0
str_disp_kbdmse		.db	"Displaying Keyboard & Mouse Activity",13,10
			.db	"  Press keys on test keyboard to display scan codes",13,10
			.db	"  Move mouse and click mouse buttons",13,10
			.db	"  Press <esc> on CP/M console to end",13,10,13,10,0
str_kbd_failed		.db	"***** KEYBOARD HARDWARE ERROR *****",13,10,13,10
			.db	"A basic hardware or configuration issue prevented",13,10
			.db	"the completion of the full set of keyboard tests.",13,10
			.db	"Check your hardware and verify the port",13,10
			.db	"addresses being used for the controller",0
str_mse_failed		.db	"***** MOUSE HARDWARE ERROR *****",13,10,13,10
			.db	"A basic hardware or configuration issue prevented",13,10
			.db	"the completion of the full set of mouse tests.",13,10
			.db	"Check your hardware and verify the port",13,10
			.db	"addresses being used for the controller",0
str_kbdmse_failed	.db	"***** KEYBOARD/MOUSE HARDWARE ERROR *****",13,10,13,10
			.db	"A basic hardware or configuration issue prevented",13,10
			.db	"the completion of the full set of keyboard/mouse tests.",13,10
			.db	"Check your hardware and verify the port",13,10
			.db	"addresses being used for the controller",0
;
;=======================================================================
; Working data
;=======================================================================
;
stksav		.dw	0		; stack pointer saved at start
		.fill	stksiz,0	; stack
stack		.equ	$		; stack top
;
iocmd		.db	0
iodat		.db	0
;
workbuf		.fill	8
workbuf_len	.db	0
;
msebuf		.fill	5,0
msebuflen	.db	0
;
mse_stat	.db	0
mse_x		.dw	0
mse_y		.dw	0
;
kbd_byte	.db	0
;
ctlr_cfgval	.db	0		; Value for controller cmd reg 0
;
cpuscl		.db	cpumhz - 2
timeout		.db	ltimout
;
;=======================================================================
;
	.end