emailler/client/ip65/tftp.s

580 lines
16 KiB
ArmAsm

;minimal tftp implementation (client only)
;supports file upload and download
TFTP_MAX_RESENDS=10
TFTP_TIMER_MASK=$F8 ;mask lower two bits, means we wait for 8 x1/4 seconds
.include "../inc/common.i"
.ifndef NB65_API_VERSION_NUMBER
.define EQU =
.include "../inc/nb65_constants.i"
.endif
.exportzp tftp_filename
.export tftp_load_address
.export tftp_ip
.export tftp_download
.export tftp_upload
.export tftp_data_block_length
.export tftp_set_callback_vector
.export tftp_data_block_length
.export tftp_clear_callbacks
.export tftp_filesize
.export tftp_upload_from_memory
.import ip65_process
.import ip65_error
.import udp_add_listener
.import udp_remove_listener
.import output_buffer
.import udp_callback
.import udp_send
.import check_for_abort_key
.import udp_inp
.import ip_inp
.importzp ip_src
.importzp udp_src_port
.importzp udp_data
.import udp_send_dest
.import udp_send_src_port
.import udp_send_dest_port
.import udp_send_len
.import copymem
.importzp copy_src
.importzp copy_dest
.import timer_read
.segment "IP65ZP" : zeropage
tftp_filename: .res 2 ;name of file to d/l or filemask to get directory listing for
.bss
;packet offsets
tftp_inp = udp_inp + udp_data
tftp_outp = output_buffer
;everything after filename in a request at a relative address, not fixed, so don't bother defining offset constants
tftp_server_port=69
tftp_client_port_low_byte: .res 1
tftp_load_address: .res 2 ;address file will be (or was) downloaded to
tftp_ip: .res 4 ;ip address of tftp server - set to 255.255.255.255 (broadcast) to send request to all tftp servers on local lan
tftp_data_block_length: .res 2
tftp_send_len: .res 2
tftp_current_memloc: .res 2
; tftp state machine
tftp_initializing = 1 ; initial state
tftp_initial_request_sent=2 ; sent the RRQ or WRQ, waiting for some data
tftp_transmission_in_progress=3 ; we have sent/received the first packet of file data
tftp_complete=4 ; we have sent/received the final packet of file data
tftp_error=5 ; we got an error
tftp_state: .res 1 ; current activity
tftp_timer: .res 1
tftp_resend_counter: .res 1
tftp_break_inner_loop: .res 1
tftp_current_block_number: .res 2
tftp_actual_server_port: .res 2 ;this is read from the reply - it is not (usually) the port # we send the RRQ or WRQ to
tftp_actual_server_ip: .res 4 ;this is read from the reply - it may not be the IP we sent to (e.g. if we send to broadcast)
tftp_just_set_new_load_address: .res 1
tftp_opcode: .res 2 ; will be set to 4 if we are doing a RRQ, or 7 if we are doing a DIR
tftp_filesize: .res 2 ;will be set by tftp_download, needs to be set before calling tftp_upload_from_memory
tftp_bytes_remaining: .res 2
.code
;uploads a file to a tftp server with data retrieved from specified memory location
; inputs:
; tftp_ip: ip address of host to send file to (set to 255.255.255.255 for broadcast)
; tftp_filename: pointer to null terminated name of file to upload
; tftp_load_address: starting address of data to be sent
; tftp_filesize: length of data to send
; outputs: carry flag is set if there was an error
; if a callback vector has been set with tftp_set_callback_vector
; then the specified routine will be called once for each 512 byte packet
; sent from the tftp server (each time AX will point at data block just arrived,
; and tftp_data_block_length will contain number of bytes in that data block)
; otherwise, the buffer at tftp_load_address will be filled
; with file downloaded.
; tftp_load_address: will be set to the actual address loaded into (NB - this field is
; ignored if a callback vector has been set with tftp_set_callback_vector)
tftp_upload_from_memory:
ldax #copy_ram_to_tftp_block
jsr tftp_set_callback_vector
ldax tftp_filesize
stax tftp_bytes_remaining
lda #00
sta tftp_filesize
sta tftp_filesize+1
;uploads a file to a tftp server with data retrieved from user supplied routine
; inputs:
; tftp_ip: ip address of host to send file to (set to 255.255.255.255 for broadcast)
; tftp_filename: pointer to null terminated name of file to upload
; a callback vector should have been set with tftp_set_callback_vector
; outputs: carry flag is set if there was an error
; the specified routine will be called once for each 512 byte packet
; to be sent from the tftp server.
tftp_upload:
ldax #$0200 ;opcode 02 = WRQ
jmp set_tftp_opcode
;download a file from a tftp server
; inputs:
; tftp_ip: ip address of host to download from (set to 255.255.255.255 for broadcast)
; tftp_filename: pointer to null terminated name of file to download
; tftp_load_address: memory location that dir will be stored in, or $0000 to
; treat first 2 bytes received from tftp server as memory address that rest
; of file should be loaded into (e.g. if downloading a C64 'prg' file)
; outputs: carry flag is set if there was an error
; if a callback vector has been set with tftp_set_callback_vector
; then the specified routine will be called once for each 512 byte packet
; sent from the tftp server (each time AX will point at data block just arrived,
; and tftp_data_block_length will contain number of bytes in that data block)
; otherwise, the buffer at tftp_load_address will be filled
; with file downloaded.
; tftp_load_address: will be set to the actual address loaded into (NB - this field is
; ignored if a callback vector has been set with tftp_set_callback_vector)
tftp_download:
lda #00
sta tftp_filesize
sta tftp_filesize+1
ldx #$01 ;opcode 01 = RRQ (A should already be zero from having just reset file length)
set_tftp_opcode:
stax tftp_opcode
lda #tftp_initializing
sta tftp_state
ldax #0000
stax tftp_current_block_number
ldax tftp_load_address
stax tftp_current_memloc
ldax #tftp_in
stax udp_callback
ldx #$69
inc tftp_client_port_low_byte ;each transfer uses a different client port
lda tftp_client_port_low_byte ;so we don't get confused by late replies to a previous call
jsr udp_add_listener
bcc :+ ;bail if we couldn't listen on the port we want
lda #NB65_ERROR_PORT_IN_USE
sta ip65_error
rts
:
lda #TFTP_MAX_RESENDS
sta tftp_resend_counter
@outer_delay_loop:
jsr timer_read
txa
and #TFTP_TIMER_MASK
sta tftp_timer ;we only care about the high byte
lda #0
sta tftp_break_inner_loop
lda tftp_state
cmp #tftp_initializing
bne @not_initializing
jsr send_request_packet
jmp @inner_delay_loop
@not_initializing:
cmp #tftp_error
bne @not_error
@exit_with_error:
ldx #$69
lda tftp_client_port_low_byte
jsr udp_remove_listener
sec
rts
@not_error:
cmp #tftp_complete
bne @not_complete
jsr send_ack ;send the ack for the last block
ldx #$69
lda tftp_client_port_low_byte
jsr udp_remove_listener
rts
@not_complete:
cmp #tftp_transmission_in_progress
bne @not_transmitting
jsr send_tftp_packet
jmp @inner_delay_loop
@not_transmitting:
jsr send_request_packet
@inner_delay_loop:
jsr ip65_process
jsr check_for_abort_key
bcc @no_abort
lda #NB65_ERROR_ABORTED_BY_USER
sta ip65_error
jmp @exit_with_error
@no_abort:
lda tftp_break_inner_loop
bne @outer_delay_loop
jsr timer_read
txa
and #TFTP_TIMER_MASK
cmp tftp_timer
beq @inner_delay_loop
dec tftp_resend_counter
bne @outer_delay_loop
lda #NB65_ERROR_TIMEOUT_ON_RECEIVE
sta ip65_error
jmp @exit_with_error
send_request_packet:
lda #tftp_initializing
sta tftp_state
ldax tftp_opcode
stax tftp_outp
ldx #$01 ;we inc x/y at start of loop, so
ldy #$ff ;set them to be 1 below where we want the copy to begin
@copy_filename_loop:
inx
iny
bmi @error_in_send ;if we get to 0x80 bytes, we've gone too far
lda (tftp_filename),y
sta tftp_outp,x
bne @copy_filename_loop
ldy #$ff
@copy_mode_loop:
inx
iny
lda tftp_octet_mode,y
sta tftp_outp,x
bne @copy_mode_loop
inx
txa
ldx #0
stax udp_send_len
ldx #$69
lda tftp_client_port_low_byte
stax udp_send_src_port
ldx #3 ; set destination address
: lda tftp_ip,x
sta udp_send_dest,x
dex
bpl :-
ldax #tftp_server_port ; set destination port
stax udp_send_dest_port
ldax #tftp_outp
jsr udp_send
bcs @error_in_send
lda #tftp_initial_request_sent
sta tftp_state
rts
@error_in_send:
lda #NB65_ERROR_TRANSMIT_FAILED
sta ip65_error
sec
rts
send_ack:
ldax #$0400 ;opcode 04 = ACK
stax tftp_outp
ldax #04
stax tftp_send_len
send_tftp_packet: ;TFTP block should be created in tftp_outp, we just add the UDP&IP stuff and send
ldx tftp_current_block_number
lda tftp_current_block_number+1
stax tftp_outp+2
ldx #$69
lda tftp_client_port_low_byte
stax udp_send_src_port
lda tftp_actual_server_ip
sta udp_send_dest
lda tftp_actual_server_ip+1
sta udp_send_dest+1
lda tftp_actual_server_ip+2
sta udp_send_dest+2
lda tftp_actual_server_ip+3
sta udp_send_dest+3
ldx tftp_actual_server_port
lda tftp_actual_server_port+1
stax udp_send_dest_port
ldax tftp_send_len
stax udp_send_len
ldax #tftp_outp
jsr udp_send
rts
got_expected_block:
lda tftp_current_block_number
inc tftp_current_block_number
bne :+
inc tftp_current_block_number+1
:
lda #tftp_transmission_in_progress
sta tftp_state
lda #TFTP_MAX_RESENDS
sta tftp_resend_counter
lda #1
sta tftp_break_inner_loop
ldax udp_inp+udp_src_port
stax tftp_actual_server_port
ldax ip_inp+ip_src
stax tftp_actual_server_ip
ldax ip_inp+ip_src+2
stax tftp_actual_server_ip+2
rts
tftp_in:
lda tftp_inp+1 ;get the opcode
cmp #5
bne @not_an_error
@recv_error:
lda #tftp_error
sta tftp_state
lda #NB65_ERROR_TRANSMISSION_REJECTED_BY_PEER
sta ip65_error
rts
@not_an_error:
cmp #3
beq :+
jmp @not_data_block
:
lda #0
sta tftp_just_set_new_load_address ;clear the flag
clc
lda tftp_load_address
adc tftp_load_address+1 ;is load address currently $0000?
bne @dont_set_load_address
lda tftp_callback_address_set ;have we overridden the default handler?
bne @dont_set_load_address ;if so, don't skip the first two bytes in the file
ldax udp_inp+$0c ;get first two bytes of data
stax tftp_load_address ;make them the new load adress
stax tftp_current_memloc ;also the current memory destination
lda #1 ;set the flag
sta tftp_just_set_new_load_address
@dont_set_load_address:
ldx tftp_inp+3 ;get the (low byte) of the data block
dex
cpx tftp_current_block_number
beq :+
jmp @not_expected_block_number
:
;this is the block we wanted
jsr got_expected_block
lda tftp_just_set_new_load_address
bne @skip_first_2_bytes_in_calculating_header_length
lda udp_inp+5 ;get the low byte of udp packet length
sec
sbc #$0c ;take off the length of the UDP header+OPCODE + BLOCK
jmp @adjusted_header_length
@skip_first_2_bytes_in_calculating_header_length:
lda udp_inp+5 ;get the low byte of udp packet length
sec
sbc #$0e ;take off the length of the UDP header+OPCODE + BLOCK + first 2 bytes (memory location)
@adjusted_header_length:
sta tftp_data_block_length
lda udp_inp+4 ;get high byte of the length of the UDP packet
sbc #0
sta tftp_data_block_length+1
lda tftp_just_set_new_load_address
bne @skip_first_2_bytes_in_calculating_copy_src
ldax #udp_inp+$0c
jmp @got_pointer_to_tftp_data
@skip_first_2_bytes_in_calculating_copy_src:
ldax #udp_inp+$0e
@got_pointer_to_tftp_data:
stax copy_src
ldax #output_buffer+2
stax copy_dest
ldax tftp_data_block_length
stax output_buffer
jsr copymem
ldax #output_buffer
jsr tftp_callback_vector
jsr send_ack
lda udp_inp+4 ;check the length of the UDP packet
cmp #02
bne @last_block
lda udp_inp+5
cmp #$0c
bne @last_block
beq @not_last_block
@not_data_block:
cmp #4 ;ACK is opcode 4
beq :+
jmp @not_ack
:
;it's an ACK, so we must be sending a file
ldx tftp_inp+3 ;get the (low byte) of the data block
cpx tftp_current_block_number
beq :+
jmp @not_expected_block_number
:
;the last block we sent was acked so now we need to send the next one
;
ldax #output_buffer+4
jsr tftp_callback_vector ;this (caller supplied) routine should fill the buffer with up to 512 bytes
stax tftp_data_block_length
clc
adc #4
bcc :+
inx
:
stax tftp_send_len
ldax #$0300 ;opcode 03 = DATA
stax tftp_outp
jsr got_expected_block
jsr send_tftp_packet
lda tftp_data_block_length+1 ;get length of data we just sent (high byte)
cmp #2
bne @last_block
@not_last_block:
inc tftp_filesize+1 ;add $200 to file size
inc tftp_filesize+1 ;add $200 to file size
@not_ack:
@not_expected_block_number:
rts
@last_block:
lda tftp_data_block_length
sta tftp_filesize; this must be the first block that is not a multiple of 512, hence till now the low byte in tftp_filesize is still $00
lda tftp_data_block_length+1 ;this can only be 0 or 1
beq :+
inc tftp_filesize+1
:
lda #tftp_complete
sta tftp_state
rts
;default handler when block arrives:
;copy to RAM
;assumes tftp_data_block_length has been set, and AX should point to start of data
copy_tftp_block_to_ram:
clc
adc #02 ;skip the 2 byte length at start of buffer
bcc :+
inx
:
stax copy_src
ldax tftp_current_memloc
stax copy_dest
ldax tftp_data_block_length
jsr copymem
clc
lda tftp_data_block_length ;update the location where the next data will go
adc tftp_current_memloc
sta tftp_current_memloc
lda tftp_data_block_length+1
adc tftp_current_memloc+1
sta tftp_current_memloc+1
rts
;default handler for uploading a file
copy_ram_to_tftp_block:
stax copy_dest
ldax tftp_current_memloc
stax copy_src
clc
lda tftp_bytes_remaining+1
beq @last_block
cmp #01
beq @last_block
dec tftp_bytes_remaining+1
dec tftp_bytes_remaining+1
ldax #$0200
@length_is_set:
stax tftp_data_block_length
jsr copymem
inc tftp_current_memloc+1
inc tftp_current_memloc+1
ldax tftp_data_block_length
clc
rts
@last_block:
ldax tftp_bytes_remaining
jmp @length_is_set
;set up vector of routine to be called when each 512 packet arrives from tftp server
;when downloading OR for routine to be called when ready to send new block
;when uploading.
;when vector is called when downloading, AX will point to data that was downloaded,
;tftp_data_block_length will be set to length of downloaded data block. This will be
;equal to $200 (512) for each block EXCEPT the final block. THe final block will
;always be less than $200 bytes - if the file is an exact multiple if $200 bytes
;long, then a final block will be received with length $00.
;when vector is called when uploading, AX will point to a 512 byte buffer that
;should be filled with the next block. the user supplied routine should set AX
;to be equal to the actual number of bytes inserted into the buffer, which should
;equal to $200 (512) for each block EXCEPT the final block. The final block must
;always be less than $200 bytes - if the file is an exact multiple if $200 bytes
;long, then a final block must be created with length $00.
; inputs:
; AX - address of routine to call for each packet.
; outputs: none
tftp_set_callback_vector:
stax tftp_callback_vector+1
rts
;clear callback vectors, i.e. all future transfers read from/write to RAM
;inputs: none
;outputs: none
tftp_clear_callbacks:
lda #0
sta tftp_callback_address_set
ldax #copy_tftp_block_to_ram
jmp tftp_set_callback_vector
.rodata
tftp_octet_mode: .asciiz "OCTET"
.data
tftp_callback_vector:
jmp copy_tftp_block_to_ram ;vector for action to take when a data block received (default is to store block in RAM)
tftp_callback_address_set: .byte 0