File Coverage

blib/lib/Device/SerialPort/Xmodem.pm
Criterion Covered Total %
statement 15 326 4.6
branch 0 88 0.0
condition 0 72 0.0
subroutine 5 39 12.8
pod n/a
total 20 525 3.8


line stmt bran cond sub pod time code
1 1     1   20779 use 5.004;
  1         3  
  1         31  
2 1     1   5 use strict;
  1         1  
  1         28  
3 1     1   4 use warnings;
  1         6  
  1         256  
4              
5             our $VERSION = '1.03';
6              
7             # Preloaded methods go here.
8              
9             package Device::SerialPort::Xmodem::Constants;
10              
11             # Define constants used in xmodem blocks
12             sub nul () { 0x00 } # ^@
13             sub soh () { 0x01 } # ^A
14             sub stx () { 0x02 } # ^B
15             sub eot () { 0x04 } # ^D
16             sub ack () { 0x06 } # ^E
17             sub nak () { 0x15 } # ^U
18             sub can () { 0x18 } # ^X
19             sub C () { 0x43 }
20             sub ctrl_z () { 0x1A } # ^Z
21              
22             sub CHECKSUM () { 1 }
23             sub CRC16 () { 2 }
24             sub CRC32 () { 3 }
25              
26             sub XMODEM () { 0x01 }
27             sub XMODEM_1K () { 0x02 }
28             sub XMODEM_CRC () { 0x03 }
29             #sub YMODEM () { 0x04 }
30             #sub ZMODEM () { 0x05 }
31              
32             package Device::SerialPort::Xmodem::Block;
33              
34 1     1   1595 use overload q[""] => \&to_string;
  1         996  
  1         7  
35              
36             # Create a new block object
37             sub new {
38 0     0     my($proto, $num, $data, $length) = @_;
39 0   0       my $class = ref $proto || $proto;
40              
41             # Check is block had required number of parameters
42 0 0         if (@_ < 3) {
43             # Return 0 length block
44 0           $length = 0;
45             } else {
46             # Define block type (128 or 1k chars) if not specified
47 0 0 0       $length ||= ( length $data > 128 ? 1024 : 128 );
48             }
49              
50             # Define structure of a Xmodem transfer block object
51 0 0         my $self = {
    0          
52             number => defined $num ? $num : 0,
53             'length'=> $length,
54             data => defined $data ? substr($data, 0, $length) : "", # Blocks are limited to 128 or 1024 chars
55             };
56              
57 0           bless $self, $class;
58             }
59              
60             # Calculate checksum of current block data
61             sub checksum {
62 0     0     my $self = $_[0];
63 0           my $sum = 0;
64 0           foreach my $c ( $self->data() ) {
65 0           $sum += ord $c;
66 0           $sum %= 256;
67             }
68 0           return $sum % 256;
69             }
70              
71             # Calculate CRC 16 bit on block data
72             sub crc16 {
73 0     0     my $self = $_[0];
74 0           return unpack('%C16*' => $self->data()) % 65536;
75             }
76              
77             # Calculate CRC 32 bit on block data
78             sub crc32 {
79 0     0     my $self = $_[0];
80 0           return unpack('%C32' => $self->data());
81             }
82              
83             # Return data one char at a time
84             sub data {
85 0     0     my $self = $_[0];
86             return wantarray
87 0 0         ? split(//, $self->{data})
88             : substr($self->{data}, 0, $self->{'length'})
89             }
90              
91             sub number {
92 0     0     my $self = $_[0];
93 0           return $self->{number};
94             }
95              
96             # Calculate checksum/crc for the current block and stringify block for transfer
97             sub to_string {
98 0     0     my $self = $_[0];
99 0           my $block_num = $self->number();
100              
101             # Assemble block to be transferred
102 0           my $xfer = pack(
103              
104             'cccA128c',
105            
106             Device::SerialPort::Xmodem::Constants::soh,
107              
108             $block_num, # Block number
109              
110             $block_num ^ 0xFF, # 2's complement of block number
111              
112             scalar $self->data, # Data chars
113              
114             $self->checksum() # Final checksum (or crc16 or crc32)
115             );
116              
117 0           return $xfer;
118             }
119              
120             #
121             # verify( type, value )
122             # ex.: verify( 'checksum', 0x7F )
123             # ex.: verify( 'crc16', 0x8328 )
124             #
125             sub verify {
126 0     0     my($self, $type, $value) = @_;
127 0           my $good_value;
128             # Detect type of value to be checked
129              
130             # TODO use new constants
131              
132 0 0         $type = 'checksum' unless defined $type;
133              
134 0 0         if( $type eq 'checksum' ) {
    0          
    0          
135 0           $good_value = $self->checksum();
136             } elsif( $type eq 'crc16' ) {
137 0           $good_value = $self->crc16();
138             } elsif( $type eq 'crc32' ) {
139 0           $good_value = $self->crc32();
140             } else {
141 0           $good_value = $self->checksum();
142             }
143 0           return $good_value == $value;
144             }
145              
146             # ----------------------------------------------------------------
147              
148             package Device::SerialPort::Xmodem::Buffer;
149              
150             sub new {
151 0     0     my($proto, $num, $data) = @_;
152 0   0       my $class = ref $proto || $proto;
153              
154             # Define structure of a Xmodem transfer buffer
155 0           my $self = [];
156 0           bless($self);
157 0           return $self;
158             }
159              
160             # Push, pop, operations on buffer
161             sub push {
162 0     0     my $self = $_[0];
163 0           my $block = $_[1];
164 0           push @$self, $block;
165             }
166              
167             sub pop {
168 0     0     my $self = $_[0];
169 0           pop @$self
170             }
171              
172             # Get last block on buffer (to retransmit / re-receive)
173             sub last {
174 0     0     my $self = $_[0];
175 0           return $self->[ $#$self ];
176             }
177              
178             sub blocks {
179 0     0     return @{$_[0]};
  0            
180             }
181              
182             #
183             # Replace n-block with given block object
184             #
185             sub replace {
186 0     0     my $self = $_[0];
187 0           my $num = $_[1];
188 0           my $block = $_[2];
189              
190 0           $self->[$num] = $block;
191             }
192              
193             sub dump {
194 0     0     my $self = $_[0];
195 0           my $output;
196            
197             # Join all blocks into string
198 0           for (my $pos = 0; $pos < scalar($self->blocks()); $pos++) {
199 0           $output .= $self->[$pos]->data();
200             }
201            
202             # Clean out any end of file markers (^Z) in data
203 0           $output =~ s/\x1A*$//;
204            
205 0           return $output;
206             }
207              
208             # ----------------------------------------------------------------
209              
210             package Device::SerialPort::Xmodem::Send;
211              
212 1     1   863 use Fcntl qw(:DEFAULT :flock);
  1         2  
  1         9675  
213              
214             # Define default timeouts for CRC handshaking stage and checksum normal procedure
215             sub TIMEOUT_CRC () { 3 };
216             sub TIMEOUT_CHECKSUM () { 10 };
217              
218             our $TIMEOUT = TIMEOUT_CRC;
219             our $DEBUG = 0;
220              
221             sub new {
222 0     0     my $proto = shift;
223 0           my %opt = @_;
224 0   0       my $class = ref $proto || $proto;
225              
226             # If port does not exist fail
227 0           _log('port = ', $opt{port});
228 0 0         if( ! exists $opt{port} ) {
229 0           _log('No valid port given, giving up.');
230 0           return 0;
231             }
232              
233 0           my $self = {
234             _port => $opt{port},
235             _filename => $opt{filename},
236             current_block => 0,
237             timeouts => 0,
238             };
239              
240 0           bless $self, $class;
241             }
242              
243             sub start {
244 0     0     my $self = $_[0];
245 0           my $port = $self->{_port};
246 0   0       my $file = $_[1] || $self->{_filename};
247 0   0       my $protocol = $_[2] || Device::SerialPort::Xmodem::Constants::XMODEM();
248              
249 0           _log('[start] checking modem[', $port, '] or file[', $file, '] members');
250 0 0 0       return 0 unless $port and $file;
251              
252             # Initialize transfer
253 0           $self->{current_block} = 0;
254 0           $self->{timeouts} = 0;
255 0           $self->{aborted} = 0;
256 0           $self->{complete} = 0;
257              
258             # Initialize a receiving buffer
259 0           _log('[start] creating new receive buffer');
260              
261 0           my $buffer = Device::SerialPort::Xmodem::Buffer->new();
262              
263 0           $self->{current_block} = Device::SerialPort::Xmodem::Block->new(0);
264              
265             # Attempt to handshake
266 0 0         return undef unless $self->handshake();
267            
268             # Open input file
269 0           my $fstatus_open = open(INFILE, '<' . $file);
270            
271             # If file does not open die gracefully
272 0 0         if (!$fstatus_open) {
273 0           _log('Error: cannot open file for reading, aborting transfer.\n');
274 0           $self->abort_transfer();
275 0           return undef;
276             }
277            
278             # Get file lock
279 0           my $fstatus_lock = flock(INFILE, LOCK_SH);
280              
281             # If file does not lock complain but carry on
282 0 0         if (!$fstatus_lock) {
283 0           _log('Warning: file could not be locked, proceeding anyhow.\n');
284             }
285              
286             # Create first block
287 0           my $block_data = undef;
288 0           seek(INFILE, 0, 0);
289 0           read(INFILE, $block_data, 128, 0);
290 0           _log('[start] creating first data block [', unpack('H*',$block_data), '] data');
291 0           $self->{current_block} = Device::SerialPort::Xmodem::Block->new(0x01, $block_data);
292            
293             # Main send cycle (subsequent timeout cycles)
294 0   0       do {
      0        
295            
296 0           _log('doing loop\n');
297            
298 0           $self->send_message($self->{current_block}->to_string());
299            
300 0           my %message = $self->receive_message();
301            
302 0 0         if ( $message{type} eq Device::SerialPort::Xmodem::Constants::ack() ) {
303             # Received Ack, if more file remains send more
304 0           _log('[start] received : ', $message{type}, ', sending preparing next block.\n');
305 0           _log('building new block at ', ($self->{current_block}->number() * 128), ', 128 long.\n');
306 0           seek(INFILE, ($self->{current_block}->number() * 128), 0);
307 0           my $block_data = undef;
308 0           my $bytes_read = read(INFILE, $block_data, 128, 0);
309 0 0         if ($bytes_read != 0) {
310             # Not EOT create next block
311 0           _log('blocks read: ', $bytes_read, ', total length: ', length($block_data), '.\n');
312 0           while (length($block_data) < 128) {
313 0           _log('padding block_data');
314 0           $block_data .= chr(0x1a);
315             }
316 0           _log('blocks read: ', $bytes_read, ', total length: ', length($block_data), '.\n');
317 0           _log('[start] creating new data block [', unpack('H*',$block_data), '] data');
318 0           _log('creating as block no ', ($self->{current_block}->number() + 1), '.\n');
319 0           $self->{current_block} = Device::SerialPort::Xmodem::Block->new( ($self->{current_block}->number() + 1), $block_data);
320 0           $self->{timeouts} = 0;
321             } else {
322             # Send EOT, we've hit the end!
323 0           $self->send_eot();
324 0           $self->{complete} = 1;
325             }
326             } else {
327             # If last block transmitted mark complete and write file
328 0           _log('[start] or assumed (garble): ', $message{type}, ', trying again.\n');
329 0           $self->{timeouts}++;
330             }
331            
332             } until (($self->{complete}) || ($self->timeouts() >= 10) || ($self->{aborted}));
333              
334 0 0         if ($self->{complete}) {
335 0           do {
336 0           my %message = $self->receive_message();
337 0 0         if ( $message{type} eq Device::SerialPort::Xmodem::Constants::ack() ) {
338 0           return 1;
339             } else {
340 0           $self->{timeouts}++;
341             }
342             } until ($self->timeouts() >= 10);
343             }
344            
345 0 0         if ($self->timeouts() >= 10) {
346 0           _log('Too many errors, giving up.\n');
347 0           $self->abort_transfer();
348 0           return undef;
349             }
350             }
351              
352             sub receive_message {
353 0     0     my $self = $_[0];
354 0           my $message_type;
355 0           my $count_in = 0;
356 0           my $received;
357 0           my $done = 0;
358 0           my $error = 0;
359            
360 0           my $receive_start_time = time;
361             # Receive answer
362 0   0       do {
363 0           my $count_in_tmp = 0;
364 0           my $received_tmp;
365 0           ($count_in_tmp, $received_tmp) = $self->port->read(1);
366 0           $received .= $received_tmp;
367 0           $count_in += $count_in_tmp;
368 0 0         if ($count_in > 0) {
    0          
369             # short message, this is all the sender should receive
370 0           $done = 1;
371             } elsif (time > $receive_start_time + 2) {
372             # wait for timeout, give the message at least a second
373 0           $error = 1;
374             }
375             } while(!$done && !$error);
376            
377 0 0         if ($error) {
378 0           _log('timeout receiving message');
379             }
380            
381 0           _log('[receive_message][', $count_in, '] received [', unpack('H*',$received), '] data');
382              
383             # Get Message Type
384 0           $message_type = ord(substr($received, 0, 1));
385            
386 0           my %message = (
387             type => $message_type, # Message Type
388             );
389            
390 0           return %message;
391             }
392              
393             sub handshake {
394 0     0     my $self = $_[0];
395 0           my $count_in = 0;
396 0           my $received;
397 0           my $done = 0;
398 0           my $error = 0;
399            
400 0           my $receive_start_time = time;
401             # Receive answer
402 0   0       do {
403 0           my $count_in_tmp = 0;
404 0           my $received_tmp;
405 0           ($count_in_tmp, $received_tmp) = $self->port->read(1);
406 0           $received .= $received_tmp;
407 0           $count_in += $count_in_tmp;
408 0 0         if ($count_in > 0) {
    0          
409             # short message, this is all the sender should receive
410 0           $done = 1;
411             } elsif (time > $receive_start_time + 11) {
412             # wait for timeout, give the message at least ten seconds
413 0           $error = 1;
414             }
415             } while(!$done && !$error);
416            
417 0 0         if ($error) {
418 0           _log('timeout waiting for handshake');
419 0           return 0;
420             }
421            
422 0           _log('[handshake][', $count_in, '] received [', unpack('H*',$received), '] data');
423              
424             # Get Message Type
425 0 0         if (ord(substr($received, 0, 1)) eq Device::SerialPort::Xmodem::Constants::nak()) {
426 0           _log('[hand shake] success');
427 0           return 1;
428             } else {
429 0           _log('[hand shake] failure');
430 0           return 0;
431             }
432             }
433              
434             sub send_message {
435             # This function sends a raw data message to the open port.
436 0     0     my $self = $_[0];
437 0           my $message = $_[1];
438 0           _log('[send_message] received [', unpack('H*',$message), '] data');
439 0           $self->port->write($message);
440 0           $self->port->write_drain();
441 0           return 1;
442             }
443              
444             sub send_eot {
445             # Send EOT character
446 0     0     my $self = $_[0];
447 0           _log('sending ');
448 0           $self->port->write( chr(Device::SerialPort::Xmodem::Constants::eot()) );
449 0           $self->port->write_drain();
450 0           return 1;
451             }
452              
453             sub abort_transfer {
454             # Send a cancel char to abort transfer
455 0     0     my $self = $_[0];
456 0           _log('aborting transfer');
457 0           $self->port->write( chr(Device::SerialPort::Xmodem::Constants::can()) );
458 0           $self->port->write_drain();
459 0           $self->{aborted} = 1;
460 0           return 1;
461             }
462              
463             sub timeouts {
464 0     0     my $self = $_[0];
465 0           $self->{timeouts};
466             }
467              
468             # Get `port' Device::SerialPort member
469             sub port {
470 0     0     $_[0]->{_port};
471             }
472              
473             sub _log {
474 0 0   0     print STDERR @_, "\n" if $DEBUG
475             }
476              
477             # ----------------------------------------------------------------
478              
479             package Device::SerialPort::Xmodem::Receive;
480              
481             # Define default timeouts for CRC handshaking stage and checksum normal procedure
482             sub TIMEOUT_CRC () { 3 };
483             sub TIMEOUT_CHECKSUM () { 10 };
484              
485             our $TIMEOUT = TIMEOUT_CRC;
486             our $DEBUG = 0;
487              
488             sub new {
489 0     0     my $proto = shift;
490 0           my %opt = @_;
491 0   0       my $class = ref $proto || $proto;
492              
493             # If port does not exist fail
494 0           _log('port = ', $opt{port});
495 0 0         if( ! exists $opt{port} ) {
496 0           _log('No valid port given, giving up.');
497 0           return 0;
498             }
499              
500 0   0       my $self = {
501             _port => $opt{port},
502             _filename => $opt{filename} || 'received.dat',
503             current_block => 0,
504             timeouts => 0,
505             };
506              
507 0           bless $self, $class;
508             }
509              
510             sub start {
511 0     0     my $self = $_[0];
512 0           my $port = $self->{_port};
513 0   0       my $file = $_[1] || $self->{_filename};
514 0   0       my $protocol = $_[2] || Device::SerialPort::Xmodem::Constants::XMODEM();
515              
516 0           _log('[start] checking modem[', $port, '] or file[', $file, '] members');
517 0 0 0       return 0 unless $port and $file;
518              
519             # Initialize transfer
520 0           $self->{current_block} = 0;
521 0           $self->{timeouts} = 0;
522 0           $self->{aborted} = 0;
523 0           $self->{complete} = 0;
524              
525             # Initialize a receiving buffer
526 0           _log('[start] creating new receive buffer');
527              
528 0           my $buffer = Device::SerialPort::Xmodem::Buffer->new();
529              
530             # Stage 1: handshaking for xmodem standard version
531 0           _log('[start] sending first timeout');
532 0           $self->send_nak();
533              
534 0           $self->{current_block} = Device::SerialPort::Xmodem::Block->new(0);
535              
536             # Open output file
537 0           my $fstatus_open = open OUTFILE, '>'.$file;
538              
539             # If file does not open die gracefully
540 0 0         if (!$fstatus_open) {
541 0           _log('Error: cannot open file for writing, aborting transfer.\n');
542 0           $self->abort_transfer();
543 0           return undef;
544             }
545            
546             # Main receive cycle (subsequent timeout cycles)
547 0   0       do {
      0        
548              
549             # Try to receive a message
550 0           my %message = $self->receive_message();
551            
552 0 0         if ( $message{type} eq Device::SerialPort::Xmodem::Constants::nul() ) {
    0          
    0          
553             # Nothing received yet, do nothing
554 0           _log('[start] ', $message{type});
555             } elsif ( $message{type} eq Device::SerialPort::Xmodem::Constants::eot() ) {
556             # If last block transmitted mark complete and write file
557 0           _log('[start] ', $message{type});
558              
559             # Acknoledge we received
560 0           $self->send_ack();
561 0           $self->{complete} = 1;
562            
563             # Write buffer data to file
564 0           print(OUTFILE $buffer->dump());
565            
566 0           close OUTFILE;
567             } elsif ( $message{type} eq Device::SerialPort::Xmodem::Constants::soh() ) {
568             # If message header, check integrity and build block
569 0           _log('[start] ', $message{type});
570 0           my $message_status = 1;
571            
572             # Check block number
573 0 0         if ( (255 - $message{complement}) != $message{number} ) {
574 0           _log('[start] bad block number: ', $message{number}, ' != (255 - ', $message{complement}, ')' );
575 0           $message_status = 0;
576             }
577            
578             # Check block numbers for out of sequence blocks
579 0 0 0       if (
      0        
      0        
      0        
580             (
581             (
582             ($message{number} < $self->{current_block}->number())
583             || ($message{number} > ($self->{current_block}->number() + 1))
584             )
585             && ($message{number} != 0x00)
586             )
587             || (
588             (
589             ($self->{current_block}->number() != 0xFF)
590             )
591             && ($message{number} == 0x00)
592             )
593             ) {
594 0           _log('[start] bad block sequence');
595 0           $self->abort_transfer();
596             }
597              
598             # Instance a new "block" object from message data received
599 0           my $new_block = Device::SerialPort::Xmodem::Block->new( $message{number}, $message{data} );
600              
601             # Check block against checksum
602 0 0 0       if (!( defined $new_block && $new_block->verify( 'checksum', $message{checksum}) )) {
603 0           _log('[start] bad block checksum');
604 0           $message_status = 0;
605             }
606            
607             # This message block was good, update current_block and push onto buffer
608 0 0         if ($message_status) {
609 0           _log('[start] received block ', $new_block->number());
610              
611             # Update current block to the one received
612 0           $self->{current_block} = $new_block;
613            
614             # Push block onto buffer
615 0           $buffer->push($self->{current_block});
616            
617             # Acknoledge we successfully received block
618 0           $self->send_ack();
619            
620             } else {
621             # Send nak since did not receive block successfully
622 0           _log('[start] message_status = 0, sending ');
623 0           $self->send_nak();
624             }
625             } else {
626 0           _log('[start] neither types found, sending timingout');
627 0           $self->send_nak();
628             }
629            
630             } until (($self->{complete}) || ($self->timeouts() >= 10) || ($self->{aborted}));
631            
632 0 0         if ($self->timeouts() >= 10) {
633 0           _log('Too many errors, giving up.\n');
634 0           $self->abort_transfer();
635 0           return undef;
636             }
637            
638 0           return 1;
639             }
640              
641             sub receive_message {
642 0     0     my $self = $_[0];
643 0           my $message_type;
644 0           my $message_number = 0;
645 0           my $message_complement = 0;
646 0           my $message_data;
647             my $message_checksum;
648 0           my $count_in = 0;
649 0           my $received;
650 0           my $done = 0;
651 0           my $error = 0;
652            
653 0           my $receive_start_time = time;
654            
655             # Receive answer
656 0   0       do {
657 0           my $count_in_tmp = 0;
658 0           my $received_tmp;
659 0           ($count_in_tmp, $received_tmp) = $self->port->read(132);
660 0           $received .= $received_tmp;
661 0           $count_in += $count_in_tmp;
662 0 0 0       if ((ord(substr($received, 0, 1)) != 1) && ($count_in > 0)) {
    0          
    0          
663             # this is a short message
664 0           $done = 1;
665             } elsif ($count_in >= 132) {
666             # this is a block
667 0           $done = 1;
668             } elsif (time > $receive_start_time + 2) {
669             # wait for timeout, give the message at least a second
670 0           $error = 1;
671             }
672             } while(!$done && !$error);
673            
674 0 0         if ($error) {
675 0           _log('timeout receiving message');
676             }
677            
678 0           _log('[receive_message][', $count_in, '] received [', unpack('H*',$received), '] data');
679              
680             # Get Message Type
681 0           $message_type = ord(substr($received, 0, 1));
682            
683             # If this is a block extract data from message
684 0 0         if( $message_type eq Device::SerialPort::Xmodem::Constants::soh() ) {
685            
686             # Check block number and its 2's complement
687 0           ($message_number, $message_complement) = ( ord(substr($received,1,1)), ord(substr($received,2,1)) );
688              
689             # Extract data string from message
690 0           $message_data = substr($received,3,128);
691            
692             # Extract checksum from message
693 0           $message_checksum = ord(substr($received, 131, 1));
694             }
695            
696 0           my %message = (
697             type => $message_type, # Message Type
698             number => $message_number, # Message Sequence Number
699             complement => $message_complement, # Message Number's Complement
700             data => $message_data, # Message Data String
701             checksum => $message_checksum, # Message Data Checksum
702             );
703            
704 0           return %message;
705             }
706              
707             sub abort_transfer {
708 0     0     my $self = $_[0];
709             # Send a cancel char to abort transfer
710 0           _log('aborting transfer');
711 0           $self->port->write( chr(Device::SerialPort::Xmodem::Constants::can()) );
712 0           $self->port->write_drain();
713 0           $self->{aborted} = 1;
714 0           return 1;
715             }
716              
717             sub send_ack {
718 0     0     my $self = $_[0];
719 0           _log('sending ack');
720 0           $self->port->write( chr(Device::SerialPort::Xmodem::Constants::ack()) );
721 0           $self->port->write_drain();
722 0           $self->{timeouts} = 0;
723 0           return 1;
724             }
725              
726             sub send_nak {
727 0     0     my $self = $_[0];
728 0           _log('sending timeout (', $self->{timeouts}, ')');
729 0           $self->port->write( chr(Device::SerialPort::Xmodem::Constants::nak()) );
730 0           $self->port->write_drain();
731 0           $self->{timeouts}++;
732 0           return 1;
733             }
734              
735             sub timeouts {
736 0     0     my $self = $_[0];
737 0           $self->{timeouts};
738             }
739              
740             # Get `port' Device::SerialPort member
741             sub port {
742 0     0     $_[0]->{_port};
743             }
744              
745             sub _log {
746 0 0   0     print STDERR @_, "\n" if $DEBUG
747             }
748              
749             1;
750             __END__