blob: c934300560c94b5808b63c9090770fa2187f7ec8 [file] [log] [blame]
Austin Schuh41baf202022-01-01 14:33:40 -08001'''
2===============================
3 XMODEM file transfer protocol
4===============================
5
6.. $Id$
7
8This is a literal implementation of XMODEM.TXT_, XMODEM1K.TXT_ and
9XMODMCRC.TXT_, support for YMODEM and ZMODEM is pending. YMODEM should
10be fairly easy to implement as it is a hack on top of the XMODEM
11protocol using sequence bytes ``0x00`` for sending file names (and some
12meta data).
13
14.. _XMODEM.TXT: doc/XMODEM.TXT
15.. _XMODEM1K.TXT: doc/XMODEM1K.TXT
16.. _XMODMCRC.TXT: doc/XMODMCRC.TXT
17
18Data flow example including error recovery
19==========================================
20
21Here is a sample of the data flow, sending a 3-block message.
22It includes the two most common line hits - a garbaged block,
23and an ``ACK`` reply getting garbaged. ``CRC`` or ``CSUM`` represents
24the checksum bytes.
25
26XMODEM 128 byte blocks
27----------------------
28
29::
30
31 SENDER RECEIVER
32
33 <-- NAK
34 SOH 01 FE Data[128] CSUM -->
35 <-- ACK
36 SOH 02 FD Data[128] CSUM -->
37 <-- ACK
38 SOH 03 FC Data[128] CSUM -->
39 <-- ACK
40 SOH 04 FB Data[128] CSUM -->
41 <-- ACK
42 SOH 05 FA Data[100] CPMEOF[28] CSUM -->
43 <-- ACK
44 EOT -->
45 <-- ACK
46
47XMODEM-1k blocks, CRC mode
48--------------------------
49
50::
51
52 SENDER RECEIVER
53
54 <-- C
55 STX 01 FE Data[1024] CRC CRC -->
56 <-- ACK
57 STX 02 FD Data[1024] CRC CRC -->
58 <-- ACK
59 STX 03 FC Data[1000] CPMEOF[24] CRC CRC -->
60 <-- ACK
61 EOT -->
62 <-- ACK
63
64Mixed 1024 and 128 byte Blocks
65------------------------------
66
67::
68
69 SENDER RECEIVER
70
71 <-- C
72 STX 01 FE Data[1024] CRC CRC -->
73 <-- ACK
74 STX 02 FD Data[1024] CRC CRC -->
75 <-- ACK
76 SOH 03 FC Data[128] CRC CRC -->
77 <-- ACK
78 SOH 04 FB Data[100] CPMEOF[28] CRC CRC -->
79 <-- ACK
80 EOT -->
81 <-- ACK
82
83YMODEM Batch Transmission Session (1 file)
84------------------------------------------
85
86::
87
88 SENDER RECEIVER
89 <-- C (command:rb)
90 SOH 00 FF foo.c NUL[123] CRC CRC -->
91 <-- ACK
92 <-- C
93 SOH 01 FE Data[128] CRC CRC -->
94 <-- ACK
95 SOH 02 FC Data[128] CRC CRC -->
96 <-- ACK
97 SOH 03 FB Data[100] CPMEOF[28] CRC CRC -->
98 <-- ACK
99 EOT -->
100 <-- NAK
101 EOT -->
102 <-- ACK
103 <-- C
104 SOH 00 FF NUL[128] CRC CRC -->
105 <-- ACK
106
107
108'''
109
110__author__ = 'Wijnand Modderman <maze@pyth0n.org>'
111__copyright__ = ['Copyright (c) 2010 Wijnand Modderman',
112 'Copyright (c) 1981 Chuck Forsberg']
113__license__ = 'MIT'
114__version__ = '0.3.2'
115
116import logging
117import time
118import sys
119from functools import partial
120import collections
121
122# Loggerr
123log = logging.getLogger('xmodem')
124
125# Protocol bytes
126SOH = bytes([0x01])
127STX = bytes([0x02])
128EOT = bytes([0x04])
129ACK = bytes([0x06])
130DLE = bytes([0x10])
131NAK = bytes([0x15])
132CAN = bytes([0x18])
133CRC = bytes([0x43]) # C
134
135
136class XMODEM(object):
137 '''
138 XMODEM Protocol handler, expects an object to read from and an object to
139 write to.
140
141 >>> def getc(size, timeout=1):
142 ... return data or None
143 ...
144 >>> def putc(data, timeout=1):
145 ... return size or None
146 ...
147 >>> modem = XMODEM(getc, putc)
148
149
150 :param getc: Function to retreive bytes from a stream
151 :type getc: callable
152 :param putc: Function to transmit bytes to a stream
153 :type putc: callable
154 :param mode: XMODEM protocol mode
155 :type mode: string
156 :param pad: Padding character to make the packets match the packet size
157 :type pad: char
158
159 '''
160
161 # crctab calculated by Mark G. Mendel, Network Systems Corporation
162 crctable = [
163 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
164 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
165 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
166 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
167 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
168 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
169 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
170 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
171 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
172 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
173 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
174 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
175 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
176 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
177 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
178 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
179 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
180 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
181 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
182 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
183 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
184 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
185 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
186 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
187 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
188 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
189 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
190 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
191 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
192 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
193 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
194 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
195 ]
196
197 def __init__(self, getc, putc, mode='xmodem', pad=b'\x1a'):
198 self.getc = getc
199 self.putc = putc
200 self.mode = mode
201 self.pad = pad
202
203 def abort(self, count=2, timeout=60):
204 '''
205 Send an abort sequence using CAN bytes.
206 '''
207 for counter in range(0, count):
208 self.putc(CAN, timeout)
209
210 def send(self, stream, retry=32, timeout=360, quiet=0, callback=None):
211 '''
212 Send a stream via the XMODEM protocol.
213
214 >>> stream = file('/etc/issue', 'rb')
215 >>> print modem.send(stream)
216 True
217
218 Returns ``True`` upon succesful transmission or ``False`` in case of
219 failure.
220
221 :param stream: The stream object to send data from.
222 :type stream: stream (file, etc.)
223 :param retry: The maximum number of times to try to resend a failed
224 packet before failing.
225 :type retry: int
226 :param timeout: The number of seconds to wait for a response before
227 timing out.
228 :type timeout: int
229 :param quiet: If 0, it prints info to stderr. If 1, it does not print any info.
230 :type quiet: int
231 :param callback: Reference to a callback function that has the
232 following signature. This is useful for
233 getting status updates while a xmodem
234 transfer is underway.
235 Expected callback signature:
236 def callback(total_packets, success_count, error_count)
237 :type callback: callable
238 '''
239
240 # initialize protocol
241 try:
242 packet_size = dict(
243 xmodem = 128,
244 xmodem1k = 1024,
245 )[self.mode]
246 except AttributeError:
247 raise ValueError("An invalid mode was supplied")
248
249 error_count = 0
250 crc_mode = 0
251 cancel = 0
252 while True:
253 char = self.getc(1)
254 if char:
255 if char == NAK:
256 crc_mode = 0
257 break
258 elif char == CRC:
259 crc_mode = 1
260 break
261 elif char == CAN:
262 if not quiet:
263 print('received CAN', file=sys.stderr)
264 if cancel:
265 return False
266 else:
267 cancel = 1
268 else:
269 log.error('send ERROR expected NAK/CRC, got %s' % \
270 (ord(char),))
271
272 error_count += 1
273 if error_count >= retry:
274 self.abort(timeout=timeout)
275 return False
276
277 # send data
278 error_count = 0
279 success_count = 0
280 total_packets = 0
281 sequence = 1
282 while True:
283 data = stream.read(packet_size)
284 if not data:
285 log.info('sending EOT')
286 # end of stream
287 break
288 total_packets += 1
289 data = data.ljust(packet_size, self.pad)
290 if crc_mode:
291 crc = self.calc_crc(data)
292 else:
293 crc = self.calc_checksum(data)
294
295 # emit packet
296 while True:
297 if packet_size == 128:
298 self.putc(SOH)
299 else: # packet_size == 1024
300 self.putc(STX)
301 self.putc(bytes([sequence]))
302 self.putc(bytes([0xff - sequence]))
303 self.putc(data)
304 if crc_mode:
305 self.putc(bytes([crc >> 8]))
306 self.putc(bytes([crc & 0xff]))
307 else:
308 self.putc(bytes([crc]))
309
310 char = self.getc(1, timeout)
311 if char == ACK:
312 success_count += 1
313 if isinstance(callback, collections.Callable):
314 callback(total_packets, success_count, error_count)
315 break
316 if char == NAK:
317 error_count += 1
318 if isinstance(callback, collections.Callable):
319 callback(total_packets, success_count, error_count)
320 if error_count >= retry:
321 # excessive amounts of retransmissions requested,
322 # abort transfer
323 self.abort(timeout=timeout)
324 log.warning('excessive NAKs, transfer aborted')
325 return False
326
327 # return to loop and resend
328 continue
329 else:
330 log.error('Not ACK, Not NAK')
331 error_count += 1
332 if isinstance(callback, collections.Callable):
333 callback(total_packets, success_count, error_count)
334 if error_count >= retry:
335 # excessive amounts of retransmissions requested,
336 # abort transfer
337 self.abort(timeout=timeout)
338 log.warning('excessive protocol errors, transfer aborted')
339 return False
340
341 # return to loop and resend
342 continue
343
344 # protocol error
345 self.abort(timeout=timeout)
346 log.error('protocol error')
347 return False
348
349 # keep track of sequence
350 sequence = (sequence + 1) % 0x100
351
352 while True:
353 # end of transmission
354 self.putc(EOT)
355
356 #An ACK should be returned
357 char = self.getc(1, timeout)
358 if char == ACK:
359 break
360 else:
361 error_count += 1
362 if error_count >= retry:
363 self.abort(timeout=timeout)
364 log.warning('EOT was not ACKd, transfer aborted')
365 return False
366
367 return True
368
369 def recv(self, stream, crc_mode=1, retry=16, timeout=60, delay=1, quiet=0):
370 '''
371 Receive a stream via the XMODEM protocol.
372
373 >>> stream = file('/etc/issue', 'wb')
374 >>> print modem.recv(stream)
375 2342
376
377 Returns the number of bytes received on success or ``None`` in case of
378 failure.
379 '''
380
381 # initiate protocol
382 error_count = 0
383 char = 0
384 cancel = 0
385 while True:
386 # first try CRC mode, if this fails,
387 # fall back to checksum mode
388 if error_count >= retry:
389 self.abort(timeout=timeout)
390 return None
391 elif crc_mode and error_count < (retry / 2):
392 if not self.putc(CRC):
393 time.sleep(delay)
394 error_count += 1
395 else:
396 crc_mode = 0
397 if not self.putc(NAK):
398 time.sleep(delay)
399 error_count += 1
400
401 char = self.getc(1, timeout)
402 if not char:
403 error_count += 1
404 continue
405 elif char == SOH:
406 #crc_mode = 0
407 break
408 elif char == STX:
409 break
410 elif char == CAN:
411 if cancel:
412 return None
413 else:
414 cancel = 1
415 else:
416 error_count += 1
417
418 # read data
419 error_count = 0
420 income_size = 0
421 packet_size = 128
422 sequence = 1
423 cancel = 0
424 while True:
425 while True:
426 if char == SOH:
427 packet_size = 128
428 break
429 elif char == STX:
430 packet_size = 1024
431 break
432 elif char == EOT:
433 # We received an EOT, so send an ACK and return the received
434 # data length
435 self.putc(ACK)
436 return income_size
437 elif char == CAN:
438 # cancel at two consecutive cancels
439 if cancel:
440 return None
441 else:
442 cancel = 1
443 else:
444 if not quiet:
445 print('recv ERROR expected SOH/EOT, got', ord(char), file=sys.stderr)
446 error_count += 1
447 if error_count >= retry:
448 self.abort()
449 return None
450 # read sequence
451 error_count = 0
452 cancel = 0
453 seq1 = ord(self.getc(1))
454 seq2 = 0xff - ord(self.getc(1))
455 if seq1 == sequence and seq2 == sequence:
456 # sequence is ok, read packet
457 # packet_size + checksum
458 data = self.getc(packet_size + 1 + crc_mode, timeout)
459 if crc_mode:
460 csum = (ord(data[-2]) << 8) + ord(data[-1])
461 data = data[:-2]
462 log.debug('CRC (%04x <> %04x)' % \
463 (csum, self.calc_crc(data)))
464 valid = csum == self.calc_crc(data)
465 else:
466 csum = data[-1]
467 data = data[:-1]
468 log.debug('checksum (checksum(%02x <> %02x)' % \
469 (ord(csum), self.calc_checksum(data)))
470 valid = ord(csum) == self.calc_checksum(data)
471
472 # valid data, append chunk
473 if valid:
474 income_size += len(data)
475 stream.write(data)
476 self.putc(ACK)
477 sequence = (sequence + 1) % 0x100
478 char = self.getc(1, timeout)
479 continue
480 else:
481 # consume data
482 self.getc(packet_size + 1 + crc_mode)
483 self.debug('expecting sequence %d, got %d/%d' % \
484 (sequence, seq1, seq2))
485
486 # something went wrong, request retransmission
487 self.putc(NAK)
488
489 def calc_checksum(self, data, checksum=0):
490 '''
491 Calculate the checksum for a given block of data, can also be used to
492 update a checksum.
493
494 >>> csum = modem.calc_checksum('hello')
495 >>> csum = modem.calc_checksum('world', csum)
496 >>> hex(csum)
497 '0x3c'
498
499 '''
500 return (sum(map(ord, data)) + checksum) % 256
501
502 def calc_crc(self, data, crc=0):
503 '''
504 Calculate the Cyclic Redundancy Check for a given block of data, can
505 also be used to update a CRC.
506
507 >>> crc = modem.calc_crc('hello')
508 >>> crc = modem.calc_crc('world', crc)
509 >>> hex(crc)
510 '0xd5e3'
511
512 '''
513 for char in data:
514 crc = (crc << 8) ^ self.crctable[((crc >> 8) ^ int(char)) & 0xff]
515 return crc & 0xffff
516
517
518XMODEM1k = partial(XMODEM, mode='xmodem1k')
519
520
521def run():
522 import optparse
523 import subprocess
524
525 parser = optparse.OptionParser(usage='%prog [<options>] <send|recv> filename filename')
526 parser.add_option('-m', '--mode', default='xmodem',
527 help='XMODEM mode (xmodem, xmodem1k)')
528
529 options, args = parser.parse_args()
530 if len(args) != 3:
531 parser.error('invalid arguments')
532 return 1
533
534 elif args[0] not in ('send', 'recv'):
535 parser.error('invalid mode')
536 return 1
537
538 def _func(so, si):
539 import select
540 import subprocess
541
542 print('si', si)
543 print('so', so)
544
545 def getc(size, timeout=3):
546 w,t,f = select.select([so], [], [], timeout)
547 if w:
548 data = so.read(size)
549 else:
550 data = None
551
552 print('getc(', repr(data), ')')
553 return data
554
555 def putc(data, timeout=3):
556 w,t,f = select.select([], [si], [], timeout)
557 if t:
558 si.write(data)
559 si.flush()
560 size = len(data)
561 else:
562 size = None
563
564 print('putc(', repr(data), repr(size), ')')
565 return size
566
567 return getc, putc
568
569 def _pipe(*command):
570 pipe = subprocess.Popen(command,
571 stdout=subprocess.PIPE, stdin=subprocess.PIPE)
572 return pipe.stdout, pipe.stdin
573
574 if args[0] == 'recv':
575 import io
576 getc, putc = _func(*_pipe('sz', '--xmodem', args[2]))
577 stream = open(args[1], 'wb')
578 xmodem = XMODEM(getc, putc, mode=options.mode)
579 status = xmodem.recv(stream, retry=8)
580 stream.close()
581
582 elif args[0] == 'send':
583 getc, putc = _func(*_pipe('rz', '--xmodem', args[2]))
584 stream = open(args[1], 'rb')
585 xmodem = XMODEM(getc, putc, mode=options.mode)
586 status = xmodem.send(stream, retry=8)
587 stream.close()
588
589if __name__ == '__main__':
590 sys.exit(run())