Tidied up the ymodem implementation, as we are only testing sending a single file at a time and no attempt is being made to keep compatibility with other *modem implmentations slimmed it down to code that is actually in use.
This commit is contained in:
parent
fe385178b7
commit
afcfde4b7b
2 changed files with 54 additions and 358 deletions
|
|
@ -135,7 +135,7 @@ def send_file(file):
|
|||
|
||||
print("Sending", file)
|
||||
try:
|
||||
print("Send completed:", modem.send([file], callback=callback))
|
||||
print("Send completed:", modem.send(file, callback=callback))
|
||||
except Exception as e:
|
||||
print("Exception", e)
|
||||
SerialPortManager.switchToCmdMode()
|
||||
|
|
|
|||
|
|
@ -1,87 +1,19 @@
|
|||
'''
|
||||
===============================
|
||||
XMODEM file transfer protocol
|
||||
===============================
|
||||
=======================================
|
||||
Modified YMODEM file transfer protocol
|
||||
=======================================
|
||||
|
||||
.. $Id$
|
||||
|
||||
This is a literal implementation of XMODEM.TXT_, XMODEM1K.TXT_ and
|
||||
XMODMCRC.TXT_, support for YMODEM and ZMODEM is pending. YMODEM should
|
||||
be fairly easy to implement as it is a hack on top of the XMODEM
|
||||
protocol using sequence bytes ``0x00`` for sending file names (and some
|
||||
meta data).
|
||||
This is a implementation of YMODEM as used on the Cosmo Communicator.
|
||||
|
||||
.. _XMODEM.TXT: doc/XMODEM.TXT
|
||||
.. _XMODEM1K.TXT: doc/XMODEM1K.TXT
|
||||
.. _XMODMCRC.TXT: doc/XMODMCRC.TXT
|
||||
|
||||
Data flow example including error recovery
|
||||
==========================================
|
||||
Data flow example
|
||||
=================
|
||||
|
||||
Here is a sample of the data flow, sending a 3-block message.
|
||||
It includes the two most common line hits - a garbaged block,
|
||||
and an ``ACK`` reply getting garbaged. ``CRC`` or ``CSUM`` represents
|
||||
the checksum bytes.
|
||||
|
||||
XMODEM 128 byte blocks
|
||||
----------------------
|
||||
|
||||
::
|
||||
|
||||
SENDER RECEIVER
|
||||
|
||||
<-- NAK
|
||||
SOH 01 FE Data[128] CSUM -->
|
||||
<-- ACK
|
||||
SOH 02 FD Data[128] CSUM -->
|
||||
<-- ACK
|
||||
SOH 03 FC Data[128] CSUM -->
|
||||
<-- ACK
|
||||
SOH 04 FB Data[128] CSUM -->
|
||||
<-- ACK
|
||||
SOH 05 FA Data[100] CPMEOF[28] CSUM -->
|
||||
<-- ACK
|
||||
EOT -->
|
||||
<-- ACK
|
||||
|
||||
XMODEM-1k blocks, CRC mode
|
||||
--------------------------
|
||||
|
||||
::
|
||||
|
||||
SENDER RECEIVER
|
||||
|
||||
<-- C
|
||||
STX 01 FE Data[1024] CRC CRC -->
|
||||
<-- ACK
|
||||
STX 02 FD Data[1024] CRC CRC -->
|
||||
<-- ACK
|
||||
STX 03 FC Data[1000] CPMEOF[24] CRC CRC -->
|
||||
<-- ACK
|
||||
EOT -->
|
||||
<-- ACK
|
||||
|
||||
Mixed 1024 and 128 byte Blocks
|
||||
------------------------------
|
||||
|
||||
::
|
||||
|
||||
SENDER RECEIVER
|
||||
|
||||
<-- C
|
||||
STX 01 FE Data[1024] CRC CRC -->
|
||||
<-- ACK
|
||||
STX 02 FD Data[1024] CRC CRC -->
|
||||
<-- ACK
|
||||
SOH 03 FC Data[128] CRC CRC -->
|
||||
<-- ACK
|
||||
SOH 04 FB Data[100] CPMEOF[28] CRC CRC -->
|
||||
<-- ACK
|
||||
EOT -->
|
||||
<-- ACK
|
||||
|
||||
YMODEM Batch Transmission Session (1 file)
|
||||
------------------------------------------
|
||||
Batch Transmission Session (1 file - not tested with more than one)
|
||||
-------------------------------------------------------------------
|
||||
|
||||
::
|
||||
|
||||
|
|
@ -89,21 +21,37 @@ YMODEM Batch Transmission Session (1 file)
|
|||
<-- C (command:rb)
|
||||
SOH 00 FF foo.c NUL[123] CRC CRC -->
|
||||
<-- ACK
|
||||
SOH 01 FE Data[1024] CRC CRC --> (probably received garbled)
|
||||
<-- C
|
||||
SOH 01 FE Data[128] CRC CRC -->
|
||||
SOH 01 FE Data[1024] CRC CRC (resend) --> (starts flash erase)
|
||||
<-- (empty read timed out)
|
||||
<-- (empty read timed out)
|
||||
<-- (empty read timed out)
|
||||
<-- C
|
||||
SOH 01 FE Data[1024] CRC CRC (resend) -->
|
||||
<-- ACK
|
||||
SOH 02 FC Data[128] CRC CRC -->
|
||||
SOH 02 FC Data[1024] CRC CRC --> (probably received garbled)
|
||||
<-- C
|
||||
SOH 02 FC Data[1024] CRC CRC (resend) -->
|
||||
<-- ACK
|
||||
SOH 03 FB Data[100] CPMEOF[28] CRC CRC -->
|
||||
SOH 03 FB Data[1000] CPMEOF[24] CRC CRC -->
|
||||
<-- ACK
|
||||
EOT -->
|
||||
<-- NAK
|
||||
EOT -->
|
||||
EOT -->
|
||||
<-- C
|
||||
EOT -->
|
||||
EOT -->
|
||||
EOT -->
|
||||
<-- C
|
||||
EOT -->
|
||||
EOT -->
|
||||
EOT -->
|
||||
<-- C
|
||||
EOT -->
|
||||
EOT -->
|
||||
EOT -->
|
||||
<-- ACK
|
||||
<-- C
|
||||
SOH 00 FF NUL[128] CRC CRC -->
|
||||
<-- ACK
|
||||
|
||||
|
||||
'''
|
||||
from __future__ import division, print_function
|
||||
|
|
@ -119,7 +67,7 @@ import platform
|
|||
import logging
|
||||
import time
|
||||
import sys
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
# Protocol bytes
|
||||
NUL = b'\x00'
|
||||
|
|
@ -137,22 +85,21 @@ CRC2 = b'\xc3'
|
|||
CRC3 = b'\x83'
|
||||
ABT = b'a' #0x61 - flash fail - abort
|
||||
|
||||
class XMODEM(object):
|
||||
class YMODEM(object):
|
||||
'''
|
||||
XMODEM Protocol handler, expects two callables which encapsulate the read
|
||||
YMODEM Protocol handler, expects two callables which encapsulate the read
|
||||
and write operations on the underlying stream.
|
||||
|
||||
Example functions for reading and writing to a serial line:
|
||||
|
||||
>>> import serial
|
||||
>>> from xmodem import XMODEM
|
||||
>>> from xmodem import YMODEM
|
||||
>>> ser = serial.Serial('/dev/ttyUSB0', timeout=0) # or whatever you need
|
||||
>>> modem = XMODEM(ser)
|
||||
>>> modem = YMODEM(ser)
|
||||
|
||||
|
||||
:param ser: serial port to read from or write to.
|
||||
:type getc: class
|
||||
:param mode: XMODEM protocol mode
|
||||
:type mode: string
|
||||
:param pad: Padding character to make the packets match the packet size
|
||||
:type pad: char
|
||||
|
|
@ -195,11 +142,10 @@ class XMODEM(object):
|
|||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
|
||||
]
|
||||
|
||||
def __init__(self, ser, mode='xmodem', pad=b'\x1a'):
|
||||
def __init__(self, ser, pad=b'\x1a'):
|
||||
self.ser = ser
|
||||
self.mode = mode
|
||||
self.pad = pad
|
||||
self.log = logging.getLogger('xmodem.XMODEM')
|
||||
self.log = logging.getLogger('codiUpdate')
|
||||
|
||||
def abort(self, count=2, timeout=60):
|
||||
'''
|
||||
|
|
@ -213,12 +159,11 @@ class XMODEM(object):
|
|||
for _ in range(count):
|
||||
self.ser.write(CAN)
|
||||
|
||||
def send(self, stream, retry=20, timeout=60, quiet=False, callback=None):
|
||||
def send(self, filename, retry=20, timeout=60, quiet=False, callback=None):
|
||||
'''
|
||||
Send a stream via the XMODEM protocol.
|
||||
Send a stream via the YMODEM protocol.
|
||||
|
||||
>>> stream = open('/etc/issue', 'rb')
|
||||
>>> print(modem.send(stream))
|
||||
>>> print(modem.send('filename'))
|
||||
True
|
||||
|
||||
Returns ``True`` upon successful transmission or ``False`` in case of
|
||||
|
|
@ -236,7 +181,7 @@ class XMODEM(object):
|
|||
:type quiet: bool
|
||||
:param callback: Reference to a callback function that has the
|
||||
following signature. This is useful for
|
||||
getting status updates while a xmodem
|
||||
getting status updates while a ymodem
|
||||
transfer is underway.
|
||||
Expected callback signature:
|
||||
def callback(total_packets, success_count, error_count)
|
||||
|
|
@ -244,15 +189,7 @@ class XMODEM(object):
|
|||
'''
|
||||
|
||||
# initialize protocol
|
||||
try:
|
||||
packet_size = dict(
|
||||
xmodem = 128,
|
||||
xmodem1k = 1024,
|
||||
ymodem = 1024,
|
||||
)[self.mode]
|
||||
except KeyError:
|
||||
raise ValueError("Invalid mode specified: {self.mode!r}"
|
||||
.format(self=self))
|
||||
packet_size = 1024
|
||||
|
||||
self.log.debug('Begin start sequence, packet_size=%d', packet_size)
|
||||
error_count = 0
|
||||
|
|
@ -295,30 +232,18 @@ class XMODEM(object):
|
|||
success_count = 0
|
||||
total_packets = 0
|
||||
header_sent = False
|
||||
if self.mode == 'ymodem':
|
||||
sequence = 0
|
||||
filenames = stream
|
||||
else:
|
||||
sequence = 1
|
||||
stream = None
|
||||
while True:
|
||||
# build packet
|
||||
if not header_sent:
|
||||
# send packet sequence 0 containing:
|
||||
# Filename Length [Modification-Date [Mode [Serial-Number]]]
|
||||
# 'stream' is actually the filename
|
||||
import os
|
||||
if len(filenames):
|
||||
filename = filenames.pop()
|
||||
stream = open(filename, 'rb')
|
||||
stat = os.stat(filename)
|
||||
data = os.path.basename(filename).encode() + NUL + str(stat.st_size).encode()
|
||||
self.log.debug('ymodem sending : "%s" len:%d', filename, stat.st_size)
|
||||
else:
|
||||
# empty file name packet terminates transmission
|
||||
filename = ''
|
||||
data = ''.encode()
|
||||
stream = None
|
||||
self.log.debug('ymodem done, sending empty header.')
|
||||
|
||||
if len(data) <= 128:
|
||||
header_size = 128
|
||||
else:
|
||||
|
|
@ -410,8 +335,6 @@ class XMODEM(object):
|
|||
return False
|
||||
|
||||
self.log.info('Transmission successful (ACK received).')
|
||||
if self.mode == 'ymodem':
|
||||
# YMODEM - close the stream
|
||||
stream.close()
|
||||
return True
|
||||
|
||||
|
|
@ -426,238 +349,13 @@ class XMODEM(object):
|
|||
return bytearray(_bytes)
|
||||
|
||||
def _make_send_checksum(self, crc_mode, data):
|
||||
assert crc_mode is 1
|
||||
_bytes = []
|
||||
if crc_mode:
|
||||
crc = self.calc_crc(data)
|
||||
_bytes.extend([crc >> 8, crc & 0xff])
|
||||
else:
|
||||
crc = self.calc_checksum(data)
|
||||
_bytes.append(crc)
|
||||
return bytearray(_bytes)
|
||||
|
||||
def recv(self, stream, crc_mode=1, retry=16, timeout=60, delay=1, quiet=0):
|
||||
'''
|
||||
Receive a stream via the XMODEM protocol.
|
||||
|
||||
>>> stream = open('/etc/issue', 'wb')
|
||||
>>> print(modem.recv(stream))
|
||||
2342
|
||||
|
||||
Returns the number of bytes received on success or ``None`` in case of
|
||||
failure.
|
||||
|
||||
:param stream: The stream object to write data to.
|
||||
:type stream: stream (file, etc.)
|
||||
:param crc_mode: XMODEM CRC mode
|
||||
:type crc_mode: int
|
||||
:param retry: The maximum number of times to try to resend a failed
|
||||
packet before failing.
|
||||
:type retry: int
|
||||
:param timeout: The number of seconds to wait for a response before
|
||||
timing out.
|
||||
:type timeout: int
|
||||
:param delay: The number of seconds to wait between resend attempts
|
||||
:type delay: int
|
||||
:param quiet: If ``True``, write transfer information to stderr.
|
||||
:type quiet: bool
|
||||
|
||||
'''
|
||||
|
||||
# initiate protocol
|
||||
error_count = 0
|
||||
char = 0
|
||||
cancel = 0
|
||||
while True:
|
||||
# first try CRC mode, if this fails,
|
||||
# fall back to checksum mode
|
||||
if error_count >= retry:
|
||||
self.log.info('error_count reached %d, aborting.', retry)
|
||||
self.abort(timeout=timeout)
|
||||
return None
|
||||
elif crc_mode and error_count < (retry // 2):
|
||||
if not self.ser.write(CRC):
|
||||
self.log.debug('recv error: putc failed, '
|
||||
'sleeping for %d', delay)
|
||||
time.sleep(delay)
|
||||
error_count += 1
|
||||
else:
|
||||
crc_mode = 0
|
||||
if not self.ser.write(NAK):
|
||||
self.log.debug('recv error: putc failed, '
|
||||
'sleeping for %d', delay)
|
||||
time.sleep(delay)
|
||||
error_count += 1
|
||||
|
||||
char = self.ser.read(1)
|
||||
if char is None:
|
||||
self.log.warn('recv error: read timeout in start sequence')
|
||||
error_count += 1
|
||||
continue
|
||||
elif char == SOH:
|
||||
self.log.debug('recv: SOH')
|
||||
break
|
||||
elif char == STX:
|
||||
self.log.debug('recv: STX')
|
||||
break
|
||||
elif char == CAN or char == CAN2:
|
||||
if cancel:
|
||||
self.log.info('Transmission canceled: received 2xCAN '
|
||||
'at start-sequence')
|
||||
return None
|
||||
else:
|
||||
self.log.debug('cancellation at start sequence.')
|
||||
cancel = 1
|
||||
else:
|
||||
error_count += 1
|
||||
|
||||
# read data
|
||||
error_count = 0
|
||||
income_size = 0
|
||||
packet_size = 128
|
||||
sequence = 1
|
||||
cancel = 0
|
||||
while True:
|
||||
while True:
|
||||
if char == SOH:
|
||||
if packet_size != 128:
|
||||
self.log.debug('recv: SOH, using 128b packet_size')
|
||||
packet_size = 128
|
||||
break
|
||||
elif char == STX:
|
||||
if packet_size != 1024:
|
||||
self.log.debug('recv: SOH, using 1k packet_size')
|
||||
packet_size = 1024
|
||||
break
|
||||
elif char == EOT:
|
||||
# We received an EOT, so send an ACK and return the
|
||||
# received data length.
|
||||
self.ser.write(ACK)
|
||||
self.log.info("Transmission complete, %d bytes",
|
||||
income_size)
|
||||
return income_size
|
||||
elif char == CAN or char == CAN2:
|
||||
# cancel at two consecutive cancels
|
||||
if cancel:
|
||||
self.log.info('Transmission canceled: received 2xCAN '
|
||||
'at block %d', sequence)
|
||||
return None
|
||||
else:
|
||||
self.log.debug('cancellation at block %d', sequence)
|
||||
cancel = 1
|
||||
else:
|
||||
err_msg = ('recv error: expected SOH, EOT; '
|
||||
'got {0!r}'.format(char))
|
||||
if not quiet:
|
||||
print(err_msg, file=sys.stderr)
|
||||
self.log.warn(err_msg)
|
||||
error_count += 1
|
||||
if error_count > retry:
|
||||
self.log.info('error_count reached %d, aborting.',
|
||||
retry)
|
||||
self.abort()
|
||||
return None
|
||||
|
||||
# read sequence
|
||||
error_count = 0
|
||||
cancel = 0
|
||||
self.log.debug('recv: data block %d', sequence)
|
||||
seq1 = self.ser.read(1)
|
||||
if seq1 is None:
|
||||
self.log.warn('read failed to get first sequence byte')
|
||||
seq2 = None
|
||||
else:
|
||||
seq1 = ord(seq1)
|
||||
seq2 = self.ser.read(1)
|
||||
if seq2 is None:
|
||||
self.log.warn('read failed to get second sequence byte')
|
||||
else:
|
||||
# second byte is the same as first as 1's complement
|
||||
seq2 = 0xff - ord(seq2)
|
||||
|
||||
if not (seq1 == seq2 == sequence):
|
||||
# consume data anyway ... even though we will discard it,
|
||||
# it is not the sequence we expected!
|
||||
self.log.error('expected sequence %d, '
|
||||
'got (seq1=%r, seq2=%r), '
|
||||
'receiving next block, will NAK.',
|
||||
sequence, seq1, seq2)
|
||||
self.ser.read(packet_size + 1 + crc_mode)
|
||||
else:
|
||||
# sequence is ok, read packet
|
||||
# packet_size + checksum
|
||||
data = self.ser.read(packet_size + 1 + crc_mode)
|
||||
valid, data = self._verify_recv_checksum(crc_mode, data)
|
||||
|
||||
# valid data, append chunk
|
||||
if valid:
|
||||
income_size += len(data)
|
||||
stream.write(data)
|
||||
self.ser.write(ACK)
|
||||
sequence = (sequence + 1) % 0x100
|
||||
# get next start-of-header byte
|
||||
char = self.ser.read(1)
|
||||
continue
|
||||
|
||||
# something went wrong, request retransmission
|
||||
self.log.warn('recv error: purge, requesting retransmission (NAK)')
|
||||
while True:
|
||||
# When the receiver wishes to <nak>, it should call a "PURGE"
|
||||
# subroutine, to wait for the line to clear. Recall the sender
|
||||
# tosses any characters in its UART buffer immediately upon
|
||||
# completing sending a block, to ensure no glitches were mis-
|
||||
# interpreted. The most common technique is for "PURGE" to
|
||||
# call the character receive subroutine, specifying a 1-second
|
||||
# timeout, and looping back to PURGE until a timeout occurs.
|
||||
# The <nak> is then sent, ensuring the other end will see it.
|
||||
data = self.ser.read(1)
|
||||
if data is None:
|
||||
break
|
||||
self.ser.write(NAK)
|
||||
# get next start-of-header byte
|
||||
char = self.ser.read(1)
|
||||
continue
|
||||
|
||||
def _verify_recv_checksum(self, crc_mode, data):
|
||||
if crc_mode:
|
||||
_checksum = bytearray(data[-2:])
|
||||
their_sum = (_checksum[0] << 8) + _checksum[1]
|
||||
data = data[:-2]
|
||||
|
||||
our_sum = self.calc_crc(data)
|
||||
valid = bool(their_sum == our_sum)
|
||||
if not valid:
|
||||
self.log.warn('recv error: checksum fail '
|
||||
'(theirs=%04x, ours=%04x), ',
|
||||
their_sum, our_sum)
|
||||
else:
|
||||
_checksum = bytearray([data[-1]])
|
||||
their_sum = _checksum[0]
|
||||
data = data[:-1]
|
||||
|
||||
our_sum = self.calc_checksum(data)
|
||||
valid = their_sum == our_sum
|
||||
if not valid:
|
||||
self.log.warn('recv error: checksum fail '
|
||||
'(theirs=%02x, ours=%02x)',
|
||||
their_sum, our_sum)
|
||||
return valid, data
|
||||
|
||||
def calc_checksum(self, data, checksum=0):
|
||||
'''
|
||||
Calculate the checksum for a given block of data, can also be used to
|
||||
update a checksum.
|
||||
|
||||
>>> csum = modem.calc_checksum('hello')
|
||||
>>> csum = modem.calc_checksum('world', csum)
|
||||
>>> hex(csum)
|
||||
'0x3c'
|
||||
|
||||
'''
|
||||
if platform.python_version_tuple() >= ('3', '0', '0'):
|
||||
return (sum(data) + checksum) % 256
|
||||
else:
|
||||
return (sum(map(ord, data)) + checksum) % 256
|
||||
|
||||
def calc_crc(self, data, crc=0):
|
||||
'''
|
||||
Calculate the Cyclic Redundancy Check for a given block of data, can
|
||||
|
|
@ -675,6 +373,4 @@ class XMODEM(object):
|
|||
return crc & 0xffff
|
||||
|
||||
|
||||
XMODEM1k = partial(XMODEM, mode='xmodem1k')
|
||||
YMODEM = partial(XMODEM, mode='ymodem')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue