Module: ModemProtocols

Defined in:
lib/modem_protocols.rb

Defined Under Namespace

Classes: RXChecksumError, RXSynchError, RXTimeout

Constant Summary collapse

XMODEM_BLOCK_SIZE =

how many bytes (ecluding header & checksum) in each block?

128
XMODEM_MAX_TIMEOUTS =

how many timeouts in a row before the sender gives up?

5
XMODEM_MAX_ERRORS =

how many errors on a single block before the receiver gives up?

10
XMODEM_CRC_ATTEMPTS =

how many times should receiver attempt to use CRC?

3
LOG_NAME =
'ModemProtocols'

Class Method Summary collapse

Class Method Details

.ccitt16_crc(block) ⇒ Object

calculate a 16-bit CRC



240
241
242
243
244
245
246
# File 'lib/modem_protocols.rb', line 240

def ModemProtocols::ccitt16_crc(block)
#cribbed from http://www.hadermann.be/blog/32/ruby-crc16-implementation/   
  raise RXChecksumError("checksum requested of invalid block {size should be #{XMODEM_BLOCK_SIZE}, was #{block.length}") unless block.length==XMODEM_BLOCK_SIZE
  crc=0
  block.each_byte{|x| crc = ((crc<<8) ^ CCITT_16[(crc>>8) ^ x])&0xffff}
  crc    
end

.timeout_secondsObject

how long does the protocol wait before giving up?



19
20
21
# File 'lib/modem_protocols.rb', line 19

def ModemProtocols::timeout_seconds
  @timeout_seconds
end

.timeout_seconds=(val) ⇒ Object



23
24
25
# File 'lib/modem_protocols.rb', line 23

def ModemProtocols::timeout_seconds=(val)
  @timeout_seconds=val
end

.xmodem_checksum(block) ⇒ Object

calculate an 8-bit XMODEM checksum this is just the sum of all bytes modulo 0x100



230
231
232
233
234
235
236
237
# File 'lib/modem_protocols.rb', line 230

def ModemProtocols::xmodem_checksum(block)
  raise RXChecksumError("checksum requested of invalid block {size should be #{XMODEM_BLOCK_SIZE}, was #{block.length}") unless block.length==XMODEM_BLOCK_SIZE
  checksum=0
  block.each_byte do |b|
    checksum=(checksum+b)%0x100
  end
  checksum
end

.xmodem_rx(remote, local, options = nil) ⇒ Object

receive a file using XMODEM protocol (block size = 128 bytes)

remote

must be an IO object connected to an XMODEM sender

local

must be an IO object - the inbound file (trimmed of any final padding) will be written to this

options

hash of options. options are: values :crc (use 16bit CRC instead of 8 bit checksum)

  • :mode=> :crc or :checksum (default is 8 bit checksum)



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/modem_protocols.rb', line 37

def ModemProtocols::xmodem_rx(remote,local,options=nil)
  

  mode = ( (options.nil?) || options[:mode].nil? ) ? :checksum : options[:mode]
  
  logger.debug "rx: XMODEM - #{mode}"
  #flush the input buffer 
  loop do
    break if (select([remote],nil,nil,0.01).nil?)
    remote.getc
  end
  
  
  #trigger sending    
  case mode
    when :crc
      XMODEM_CRC_ATTEMPTS.times do |attempt|
        remote.putc('C')          
        break unless (select([remote],nil,nil,timeout_seconds).nil?)
        #if we don't get a response, fall back to checksum mode
        if attempt==XMODEM_CRC_ATTEMPTS-1 then
          logger.warn "rx: crc-16 request failed, falling back to checksum mode"         
          remote.putc(NAK)  
          mode=:checksum
        end
      end
    else
      remote.putc(NAK)
  end
  
  expected_block=1
  error_count=0
  last_block=""
  data=""
  loop do 
    
      begin
      rx_cmd=xmodem_rx_getbyte(remote)
        
      if rx_cmd==EOT then
        remote.putc(ACK)
        trimmed_block=last_block.sub(/(\x1A+)\Z/,'')
        local<< trimmed_block#trim any trailing FILLER characters in last block
        break
      end
      
      if rx_cmd!=SOH then
        logger.warn "rx: expected SOH (0x#{SOH}) got 0x#{"%x" % rx_cmd}  - pos = #{remote.pos}"         
        next 
      end      
      
      data=""
      block=xmodem_rx_getbyte(remote)
      block_check=xmodem_rx_getbyte(remote)
      validity=:valid
      validity=:invalid unless (block_check+block)==0xFF
      
      logger.debug "rx: #{validity} block number 0x#{"%02x" % block} / block number check 0x#{"%02x" % block_check}"
      logger.debug "rx: receiving block #{block} / expected block #{expected_block}"  
      raise RXSynchError if block!=expected_block && block!=((expected_block-1) % 0x100)
      
      if (block==expected_block) && (validity==:valid) then
        local<<last_block 
        last_block=""
      end
      XMODEM_BLOCK_SIZE.times do 
        b=(xmodem_rx_getbyte(remote))
        data<<b
        Thread.pass
      end
      
      
      check_ok=false
      
      case mode
        when :crc
          rx_crc=(xmodem_rx_getbyte(remote)<<8)+xmodem_rx_getbyte(remote)
          crc=ccitt16_crc(data)
          check_ok=(crc==rx_crc)
          if !check_ok then        
            logger.warn "invalid crc-16 for block #{block}: calculated 0x#{'%04x' % crc}, got 0x#{'%02x' %  rx_crc}"
          end
        else 
          rx_checksum=xmodem_rx_getbyte(remote)
          checksum=xmodem_checksum(data)
          check_ok=(checksum==rx_checksum)
          if !check_ok then        
            logger.warn "invalid checksum for block #{block}: calculated 0x#{'%02x' % checksum}, got 0x#{'%02x' %  rx_checksum}"
          end
        end
        
        if (check_ok) then
            last_block=data              
            logger.debug "rx: #{mode} test passed for block #{block}"
            remote.putc(ACK)            
          if (block==expected_block) then
            expected_block=((expected_block+1) % 0x100)
            error_count=0 #reset the error count
          end 
        else
          remote.putc(NAK)
          error_count+=1
          logger.warn "rx: checksum error # #{error_count} / max #{XMODEM_MAX_ERRORS}"
          raise RXChecksumError.new("too many receive errors on block #{block}") if error_count>XMODEM_MAX_ERRORS
        end
    rescue RXTimeout
      error_count+=1
      logger.warn "rx: timeout error # #{error_count} / max #{XMODEM_MAX_ERRORS}"
      raise RXTimeout("too many receive errors on block #{block}").new if error_count>XMODEM_MAX_ERRORS
    end      
  end
  
  logger.info "receive complete"
end

.xmodem_tx(remote, local) ⇒ Object

send a file using standard XMODEM protocol (block size = 128 bytes) will use CRC mode if requested by sender, else use 8-bit checksum

remote

must be an IO object connected to an XMODEM receiver

local

must be an IO object containing the data to be sent



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/modem_protocols.rb', line 156

def ModemProtocols::xmodem_tx(remote,local)          
  block_number=1
  current_block=""  
  sent_eot=false
  
  XMODEM_BLOCK_SIZE.times do 
    b=(local.eof? ?  FILLER : local.getc)      
    current_block<<b.chr
    Thread.pass
  end
  checksum=xmodem_checksum(current_block) 
  mode=:checksum  
  loop do 
    logger.info "tx: waiting for ACK/NAK/CAN (eot_sent: #{sent_eot})"
    if select([remote],nil,nil,timeout_seconds*XMODEM_MAX_TIMEOUTS).nil? then        
      raise RXTimeout.new("timeout waiting for input on tx (#{timeout_seconds*XMODEM_MAX_TIMEOUTS} seconds)") unless sent_eot
      logger.info "tx: timeout waiting for ACK of EOT" 
      return
    end
    if remote.eof? then        
      logger.warn "tx: unexpected eof on input"
      break
    end
    tx_cmd=remote.getc
    logger.debug "tx: got 0x#{"%x" % tx_cmd}"      
    if tx_cmd==ACK then
      if sent_eot then
        logger.debug "tx: got ACK of EOT"
        break
      end
      
      if local.eof? then
        remote.putc(EOT)
        logger.debug "tx: got ACK of last block"
        sent_eot=true
        next
      end
      block_number=((block_number+1)%0x100) 
      current_block=""
      XMODEM_BLOCK_SIZE.times do 
        b=(local.eof? ?  FILLER : local.getc)
        current_block<<b
        Thread.pass
      end
      
    elsif (block_number==1) && (tx_cmd==CRC_MODE) then
      mode=:crc
      logger.debug "tx: using crc-16 mode"
    end
    
    next unless [ACK,NAK,CRC_MODE].include?(tx_cmd)
    logger.info "tx: sending block #{block_number}"
    remote.putc(SOH)     #start of block 
    remote.putc(block_number)    #block number
    remote.putc(0xff-block_number)    #1's complement of block number
    current_block.each_byte {|b| remote.putc(b)}
    case mode
      when :crc then
        crc=ccitt16_crc(current_block)
        remote.putc(crc>>8) #crc hi byte
        remote.putc(crc & 0xFF) #crc lo byte
        logger.debug "tx: crc-16 for block #{block_number}:#{ "%04x" % crc}"
      else
        checksum=xmodem_checksum(current_block)
        remote.putc(checksum)
        logger.debug "tx: checksum for block #{block_number}:#{ "%02x" % checksum}"
    end
    
  end
  logger.info "transmit complete (eot_sent: #{sent_eot})"
end