# ip65 technical reference

# File : ip65/tftp.s

    minimal tftp implementation (client only)
    supports file upload and download

## functions

function | description
--- | ---
tftp_callback_vector |
tftp_clear_callbacks |
    clear callback vectors, i.e. all future transfers read from/write to RAM
    inputs: none
    outputs: none
tftp_download |
    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_set_callback_vector |
    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_upload |
    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_from_memory |
    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
       to be sent to the tftp server

## variables

variable | description | size (bytes)
--- | --- | ---
tftp_data_block_length |  | 2
tftp_filename | name of file to d/l or filemask to get directory listing for | 2
tftp_filesize | will be set by tftp_download, needs to be set before calling tftp_upload_from_memory | 2
tftp_ip | ip address of tftp server - set to 255.255.255.255 (broadcast) to send request to all tftp servers on local lan | 4
tftp_load_address | address file will be (or was) downloaded to | 2

## implementation

    ;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 KPR_API_VERSION_NUMBER
      .define EQU     =
      .include "../inc/kipper_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_callback_vector
      .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
    ;   to be sent to the tftp server
    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 #KPR_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
      bcs @not_complete ;if we couldn't send the ACK (e.g. coz we need to do an ARP request) then keep looping
      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 #KPR_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 #KPR_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 #KPR_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 #KPR_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
      inc tftp_callback_address_set
      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

    ;-- LICENSE FOR tftp.s --
    ; The contents of this file are subject to the Mozilla Public License
    ; Version 1.1 (the "License"); you may not use this file except in
    ; compliance with the License. You may obtain a copy of the License at
    ; http://www.mozilla.org/MPL/
    ;
    ; Software distributed under the License is distributed on an "AS IS"
    ; basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
    ; License for the specific language governing rights and limitations
    ; under the License.
    ;
    ; The Original Code is ip65.
    ;
    ; The Initial Developer of the Original Code is Jonno Downes,
    ; jonno@jamtronix.com.
    ; Portions created by the Initial Developer are Copyright (C) 2009
    ; Jonno Downes. All Rights Reserved.
    ; -- LICENSE END --