# ip65 technical reference

# File : ip65/xmodem.s

     XMODEM file transfer

## functions

function | description
--- | ---
xmodem_receive |
    recieve a file via XMODEM (checksum mode only, not CRC)
    assumes that a tcp connection has already been set up, and that the other end is waiting to start sending
    inputs: AX points to routine to call once for each byte in downloaded file (e.g. save to disk, print to screen, whatever) - byte will be in A
     xmodem_iac_escape should be set to non-zero if the remote end escapes $FF bytes (i.e. if it is a real telnet server)
    outputs: none
xmodem_send |
    send a file via XMODEM (checksum mode only, not CRC)
    assumes that a tcp connection has already been set up, and that the other end is waiting to start receiving
    inputs: AX points to routine to call once for each byte in file to send (e.g. save to disk, print to screen, whatever) - byte will be in A, carry flag set means EOF
     xmodem_iac_escape should be set to non-zero if the remote end escapes $FF bytes (i.e. if it is a real telnet server)
    outputs: none

## variables

variable | description | size (bytes)
--- | --- | ---
xmodem_iac_escape | are IAC bytes ($FF) escaped? | 1

## implementation

    ; XMODEM file transfer

    .include "../inc/common.i"
    .ifndef KPR_API_VERSION_NUMBER
      .define EQU     =
      .include "../inc/kipper_constants.i"
    .endif

    XMODEM_BLOCK_SIZE=$80  ;how many bytes (excluding header & checksum) in each block?
    XMODEM_TIMEOUT_SECONDS=5
    XMODEM_MAX_ERRORS=10
    SOH = $01
    EOT = $04
    ACK = $06
    NAK = $15
    CAN = $18
    PAD = $1A ;padding added to end of file

    .export xmodem_receive
    .export xmodem_send

    .export xmodem_iac_escape ;are IAC bytes ($FF) escaped?

    .import ip65_process
    .import ip65_error
    .import tcp_callback
    .import copymem
    .importzp copy_src
    .importzp copy_dest
    .import tcp_send
    .import tcp_send_data_len
    .import tcp_inbound_data_ptr
    .import tcp_inbound_data_length
    .import check_for_abort_key
    .import print_a
    .import print_cr
    .import print_ascii_as_native
    .import print_hex
    .import timer_seconds

    .segment "SELF_MODIFIED_CODE"
    got_byte:
      jmp $ffff

    get_byte:
      jmp $ffff

    next_char:
      lda buffer_length
      bne @not_eof
      lda buffer_length+1
      bne @not_eof
      sec
      rts
    @not_eof:
      next_char_ptr=*+1
      lda $ffff
      pha
      inc next_char_ptr
      bne :+
      inc next_char_ptr+1
    :
      sec
      lda   buffer_length
      sbc #1
      sta   buffer_length
      lda   buffer_length+1
      sbc #0
      sta   buffer_length+1
      pla
      clc
      rts

    emit_a:
    ;put a byte to the output buffer
    ;if the byte is $FF, and xmodem_iac_escape is not zero, it is doubled
      jsr @real_emit_a
      cmp #$ff
      bne exit_emit_a
      ldx xmodem_iac_escape
      beq exit_emit_a
    @real_emit_a:
    emit_a_ptr=*+1
      sta $ffff
      inc emit_a_ptr
      bne :+
      inc emit_a_ptr+1
    :
      inc xmodem_block_buffer_length
      bne :+
      inc xmodem_block_buffer_length+1
    :
    exit_emit_a:
      rts

      .bss

    original_tcp_callback: .res 2
    getc_timeout_end: .res 1
    getc_timeout_seconds: .res 1
    buffer_length: .res 2

      .code

    ;send a file via XMODEM (checksum mode only, not CRC)
    ;assumes that a tcp connection has already been set up, and that the other end is waiting to start receiving
    ;inputs: AX points to routine to call once for each byte in file to send (e.g. save to disk, print to screen, whatever) - byte will be in A, carry flag set means EOF
    ; xmodem_iac_escape should be set to non-zero if the remote end escapes $FF bytes (i.e. if it is a real telnet server)
    ;outputs: none
    xmodem_send:
      stax get_byte+1
      jsr xmodem_transfer_setup
      lda #0
      sta at_eof

    @send_block:
      ldax #sending
      jsr print_ascii_as_native

      ldax #block_number_msg
      jsr print_ascii_as_native
      lda expected_block_number
      jsr print_hex
      jsr print_cr

    @wait_for_ack_or_nak:
      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcs @synch_error
      cmp   #ACK
      beq @got_ack
      cmp   #NAK
      beq @got_nak

    @synch_error:
      pha
      lda user_abort
      beq @no_user_abort
      pla
      jmp xmodem_transfer_exit
    @no_user_abort:

    ;flush the input buffer
      lda   #0
      sta   buffer_length
      lda   buffer_length+1

      lda #'('
      jsr print_a
      pla
      jsr print_hex
      lda #')'
      jsr print_a

      inc error_number
      ldax #sync_error_msg
      jsr print_ascii_as_native
      ldax #error_count_msg
      jsr print_ascii_as_native
      lda error_number
      jsr print_hex
      jsr print_cr
      lda error_number
      cmp #XMODEM_MAX_ERRORS
      bcc @wait_for_ack_or_nak
      lda #KPR_ERROR_TOO_MANY_ERRORS
      sta ip65_error

      jmp xmodem_transfer_exit

    @got_ack:
      inc expected_block_number
      lda at_eof
      bne @send_eot
    @got_nak:
      lda #0
      sta checksum
      sta xmodem_block_buffer_length
      sta xmodem_block_buffer_length+1
      ldax #xmodem_block_buffer
      stax emit_a_ptr

      lda #SOH
      jsr emit_a
      lda expected_block_number
      jsr emit_a
      eor #$ff
      jsr emit_a
      lda #$80
      sta block_ptr
    @copy_one_byte:
      lda at_eof
      bne @add_pad_byte

      jsr get_byte
      bcc @got_byte
      ;sec indicates EOF
      lda block_ptr
      cmp #$80  ;have we sent any data at all?
      bne @add_pad_byte

    @send_eot:
      ;if we get here, we should send an EOT, then read an ACK
      ldax #1
      stax tcp_send_data_len
      ldax #eot_packet
      jsr tcp_send

      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc      ;should be an ACK coming back, doesn't really matter if we don't see it though

      jmp xmodem_transfer_exit

    @add_pad_byte:
      lda #PAD
    @got_byte:
      pha
      clc
      adc checksum
      sta checksum
      pla
      jsr emit_a
      dec block_ptr
      bne @copy_one_byte
      lda checksum
      jsr emit_a

      ldax xmodem_block_buffer_length
      stax tcp_send_data_len
      ldax #xmodem_block_buffer
      jsr tcp_send
      bcc @send_ok
      ldax #send_error
      jsr print_ascii_as_native
      lda ip65_error
      jsr print_hex
      jsr print_cr
    @send_ok:
      jmp @send_block

      rts

    xmodem_transfer_setup:
      lda #0
      sta buffer_length
      sta buffer_length+1
      sta error_number
      sta user_abort
      lda #1
      sta expected_block_number

      ldax tcp_callback
      stax original_tcp_callback
      ldax #xmodem_tcp_callback
      stax tcp_callback
      rts

    ;recieve a file via XMODEM (checksum mode only, not CRC)
    ;assumes that a tcp connection has already been set up, and that the other end is waiting to start sending
    ;inputs: AX points to routine to call once for each byte in downloaded file (e.g. save to disk, print to screen, whatever) - byte will be in A
    ; xmodem_iac_escape should be set to non-zero if the remote end escapes $FF bytes (i.e. if it is a real telnet server)
    ;outputs: none
    xmodem_receive:

      stax got_byte+1
      jsr xmodem_transfer_setup
      jsr send_nak

    @next_block:
      lda #0
      sta block_ptr
      sta checksum
      ldax #expecting
      jsr print_ascii_as_native

      ldax #block_number_msg
      jsr print_ascii_as_native
      lda expected_block_number
      jsr print_hex
      jsr print_cr

    @wait_for_block_start:
      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcc @got_block_start
      lda user_abort
      beq @no_user_abort
      sec
      jmp xmodem_transfer_exit
    @no_user_abort:
      jsr send_nak
      inc error_number
      ldax #timeout_msg
      jsr print_ascii_as_native
      ldax #error_count_msg
      jsr print_ascii_as_native
      lda error_number
      jsr print_hex
      jsr print_cr
      lda error_number
      cmp #XMODEM_MAX_ERRORS
      bcc @wait_for_block_start
      lda #KPR_ERROR_TOO_MANY_ERRORS
      sta ip65_error
      jmp xmodem_transfer_exit
    @got_block_start:
      cmp #EOT
      bne :+
      ldax #got_eot
      jsr print_ascii_as_native

      jsr send_ack
      clc
      jmp xmodem_transfer_exit
    :

      cmp #$81 ;jamming signal BBS seems to use $81 not $01 as SOH
      beq @got_soh
      cmp #SOH
      beq @got_soh
      lda #'!'  ;we got an unexpected character
      jsr print_a
      jsr print_hex
      ;we need to clear the input buffer
    @clear_input_buffer:
      lda #'!'  ;we got an unexpected character
      jsr print_a
      lda #1
      jsr getc
      bcc @clear_input_buffer

      jmp @wait_for_block_start
    @got_soh:
      ;now get block number
      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcc :+
      jsr send_nak
      lda #'.'
      jmp print_a
      jmp @wait_for_block_start
    :
      sta actual_block_number

      ;now get block number check
      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcc :+
      lda #'.'
      jmp print_a
      jsr send_nak
      jmp @wait_for_block_start
    :
      adc actual_block_number
      cmp #$ff
      beq :+
      lda #'?'
      jsr print_a
      jmp @wait_for_block_start
    :
      ldax #receiving
      jsr print_ascii_as_native

      ldax #block_number_msg
      jsr print_ascii_as_native
      lda actual_block_number
      jsr print_hex
      jsr print_cr

    @next_byte:
      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcc :+
      jmp xmodem_transfer_exit
    :
      ldx block_ptr
      sta xmodem_block_buffer,x
      adc checksum
      sta checksum

      inc block_ptr
      lda block_ptr
      bpl @next_byte

      ldax #checksum_msg
      jsr print_ascii_as_native

      lda checksum
      jsr print_hex

      lda #'/'
      jsr print_a

      lda #XMODEM_TIMEOUT_SECONDS
      jsr getc
      bcs xmodem_transfer_exit
      sta received_checksum
      jsr print_hex
      jsr print_cr

      lda received_checksum
      cmp checksum
      beq @checksum_ok
      ;checksum error :-(
      inc error_number
      ldax #checksum_error_msg
      jsr print_ascii_as_native
      ldax #error_count_msg
      jsr print_ascii_as_native
      lda error_number
      jsr print_hex
      jsr print_cr
      lda error_number
      cmp #XMODEM_MAX_ERRORS
      bcs :+
      jmp @wait_for_block_start
    :
      lda #KPR_ERROR_TOO_MANY_ERRORS
      sta ip65_error
      jmp xmodem_transfer_exit

      jsr send_nak
      jmp @next_block

    @checksum_ok:
      lda expected_block_number
      cmp actual_block_number
      bne @skip_block_output

      lda #0
      sta block_ptr

    @output_byte:
      ldx block_ptr
      lda xmodem_block_buffer,x
      jsr got_byte
      inc block_ptr
      lda block_ptr
      bpl @output_byte

      inc expected_block_number

    @skip_block_output:
      jsr send_ack
      jmp @next_block

      clc

    xmodem_transfer_exit:
      ldax original_tcp_callback
      stax tcp_callback

      rts

    xmodem_tcp_callback:
      lda tcp_inbound_data_length+1
      cmp #$ff
      bne @not_eof
      rts
    @not_eof:

      ldax tcp_inbound_data_ptr
      stax copy_src
      ldax #xmodem_stream_buffer
      stax copy_dest
      stax next_char_ptr

      ldax tcp_inbound_data_length
      stax buffer_length
      jsr copymem
      rts

    send_nak:
      ldax #1
      stax tcp_send_data_len
      ldax #nak_packet
      jmp tcp_send

    send_ack:
      ldax #1
      stax tcp_send_data_len
      ldax #ack_packet
      jmp tcp_send

    getc:
      jsr @real_getc
      bcc :+  ;of we got an error, then bail
      rts
    :
      cmp #$ff
      beq @got_ff
      clc
      rts
    @got_ff:
      lda xmodem_iac_escape
      bne @real_getc  ;need to skip over the $FF and go read another byte
      lda #$ff
      clc
      rts

    @real_getc:
      sta getc_timeout_seconds

      clc
      jsr timer_seconds  ;time of day clock: seconds (in BCD)
      sed
      adc getc_timeout_seconds
      cmp #$60
      bcc @timeout_set
      sec
      sbc #$60
    @timeout_set:
      cld
      sta getc_timeout_end

    @poll_loop:
      jsr next_char
      bcs @no_char
      rts ;done!
    @no_char:
      jsr check_for_abort_key
      bcc @no_abort
      lda #KPR_ERROR_ABORTED_BY_USER
      sta ip65_error
      inc user_abort
      rts
    @no_abort:
      jsr ip65_process
      jsr timer_seconds  ;time of day clock: seconds
      cmp getc_timeout_end
      bne @poll_loop
      lda #00
      sec
      rts

    .rodata
      ack_packet: .byte ACK
      nak_packet: .byte NAK
      eot_packet: .byte EOT

    block_number_msg: .byte " block $",0
    expecting: .byte "expecting",0
    receiving: .byte "receiving",0
    sending: .byte "sending",0
    got_eot: .byte "end of transmission",10,0

    bad_block_number: .byte "bad block number",0
    checksum_msg: .byte "checksum $",0
    checksum_error_msg : .byte "checksum",0
    timeout_msg: .byte "timeout error",0
    sync_error_msg: .byte "sync",0
    error_count_msg: .byte " error - error count $",0
    send_error: .byte " send error - $",0

    .segment "APP_SCRATCH"
    xmodem_stream_buffer: .res 1600
    xmodem_block_buffer: .res 300
    xmodem_block_buffer_length: .res 2
    expected_block_number: .res 1
    actual_block_number: .res 1
    checksum: .res 1
    received_checksum: .res 1
    block_ptr: .res 1
    error_number: .res 1
    user_abort: .res 1
    xmodem_iac_escape: .res 1
    at_eof: .res 1
    ;-- LICENSE FOR xmodem.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 --