File Coverage

blib/lib/Device/Modem/UsRobotics.pm
Criterion Covered Total %
statement 15 242 6.2
branch 0 68 0.0
condition 0 26 0.0
subroutine 5 23 21.7
pod 1 14 7.1
total 21 373 5.6


line stmt bran cond sub pod time code
1             # Device::Modem::UsRobotics - control USR modems self mode
2             #
3             # Copyright (C) 2004-2005 Cosimo Streppone, cosimo@cpan.org
4             #
5             # This program is free software; you can redistribute it and/or
6             # modify it under the same terms as Perl itself.
7             #
8             # Additionally, this is ALPHA software, still needs extensive
9             # testing and support for generic AT commads, so use it at your own risk,
10             # and without ANY warranty! Have fun.
11             #
12             # Portions of this code are adapted from TkUsr tcl program
13             # published with the GPL by Ludovic Drolez (ldrolez@free.fr).
14             # Here is his copyright and license statements:
15             #
16             # TkUsr v0.80
17             #
18             # Copyright (C) 1998-2003 Ludovic Drolez (ldrolez@free.fr)
19             #
20             # This program is free software; you can redistribute it and/or modify
21             # it under the terms of the GNU General Public License as published by
22             # the Free Software Foundation; either version 2 of the License, or
23             # (at your option) any later version.
24             #
25             # This program is distributed in the hope that it will be useful,
26             # but WITHOUT ANY WARRANTY; without even the implied warranty of
27             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28             # GNU General Public License for more details.
29             #
30             # You should have received a copy of the GNU General Public License
31             # along with this program; if not, write to the Free Software
32             # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
33             #
34             #
35             # $Id$
36              
37             package Device::Modem::UsRobotics;
38             our $VERSION = '1.58';
39             $VERSION = eval $VERSION;
40              
41 1     1   1313 use strict;
  1         2  
  1         33  
42 1     1   5 use base 'Device::Modem';
  1         3  
  1         168  
43              
44 1     1   7 use constant DLE => chr(0x10);
  1         2  
  1         54  
45 1     1   6 use constant SUB => chr(0x1A);
  1         1  
  1         46  
46 1     1   5 use constant ETX => chr(0x03);
  1         2  
  1         2942  
47              
48             our %CACHE;
49              
50             sub dump_memory
51             {
52 0     0 0   my($self, $memtype) = @_;
53 0 0         $memtype = 2 unless defined $memtype;
54 0           my $cmd = '';
55 0 0 0       if( $memtype == 2 || $memtype eq 'messages' )
56             {
57 0           $cmd = 'MTM';
58             }
59              
60 0           $cmd = 'AT+' . $cmd . Device::Modem::CR;
61 0           $self->atsend($cmd);
62 0           $self->wait(500);
63 0           my $data = $self->answer();
64 0           $self->log->write('info', 'dumped messages memory (length: '.length($data).')');
65 0           return($data);
66             }
67              
68             #sub dump_page
69             #{
70             # my($self, $page) = @_;
71             # $self->atsend( sprintf('AT+MTP=%d'.Device::Modem::CR, 0 + $page) );
72             # $self->wait(500);
73             # my $data = $self->answer();
74             # $self->log->write('info', 'dumped memory page '.$page.' (length: '.length($data).')');
75             # return($data);
76             #}
77              
78             sub get_mem_page($)
79             {
80 0     0 0   my($self, $page) = @_;
81              
82             #
83             # get a memory page and cache it for fast retrieving
84             #
85 0 0         return $CACHE{$page} if exists $CACHE{$page};
86              
87             # Download a page
88             #set device(binary) 1
89             #fconfigure $device(dev) -translation binary
90              
91             # Get the page
92 0           $self->atsend( sprintf('AT+MTP=%d'.Device::Modem::CR, 0 + $page) );
93 0           $self->wait(100);
94              
95             # Wait for data
96 0           my $data = $self->answer();
97              
98             #set device(buffer) ""
99             # cache the page
100 0           $CACHE{$page} = $data;
101              
102             ## cancel binary mode
103             #fconfigure $device(dev) -translation auto
104             #set device(binary) 0
105              
106 0           return $data;
107             }
108              
109             sub mcc_get {
110              
111 0     0 0   my $self = $_[0];
112 0           $self->atsend('AT+MCC?'.Device::Modem::CR);
113 0           my @time;
114 0           my($ok, @data) = $self->parse_answer();
115 0 0         if( index($ok, 'OK') >= 0 )
116             {
117 0           @time = split ',', $data[0];
118 0           $self->log->write('info', sprintf('MCC: %d days, %02d hrs, %02d mins, %02d secs after last clock reset', @time) );
119 0 0         return wantarray ? @time : join(',',@time);
120             }
121             else
122             {
123 0           $self->log->write('warning', 'MCC: failed to get clock value');
124 0           return undef;
125             }
126              
127             }
128              
129             #
130             # Takes days,hrs,mins values and obtains a real time value
131             #
132             sub mcc_merge {
133 0     0 0   my($self, $d, $h, $m) = @_;
134 0           $_ += 0 for $d, $h, $m ;
135              
136 0 0 0       if( $d == $h && $h == $m && $m == 255 )
      0        
137             {
138 0           $self->log->write('warning', 'invalid time 255,255,255');
139 0           return(time());
140             }
141              
142 0           my $mcc_last = $self->mcc_last_saved();
143 0           $mcc_last += 86400 * $d + 3600 * $h + 60 * $m;
144              
145 0           $self->log->write('info', "$d days, $h hours, $m mins is real time ".localtime($mcc_last));
146 0           return($mcc_last);
147             }
148              
149             sub mcc_last_saved {
150 0     0 0   my $self = $_[0];
151 0           my $dir = $self->_createSettingsDir();
152 0           my $mcc_basetime = undef;
153              
154 0 0         if( ! -d $dir )
    0          
155             {
156 0           return undef;
157             }
158             elsif( open SETTINGS, "$dir/mcc_timer" )
159             {
160 0           chomp($mcc_basetime = );
161             }
162              
163 0           $self->log->write('info', 'last mcc timer saved at '.localtime($mcc_basetime));
164 0           return($mcc_basetime);
165             }
166              
167             sub mcc_reset {
168 0     0 0   my $self = $_[0];
169 0           $self->atsend('AT+MCC'.Device::Modem::CR);
170 0           my($ok, $ans) = $self->parse_answer();
171 0           $self->log->write('info', 'internal timer reset to 00 days, 00 hrs, 00 mins');
172 0 0         if( index($ok, 'OK') >= 0 )
173             {
174             # Create settings dir
175 0           my $dir = $self->_createSettingsDir();
176 0 0         if( -d $dir )
177             {
178 0 0         if( open SETTINGS, "> $dir/mcc_timer" )
179             {
180 0           print SETTINGS time(), "\n";
181 0           $ok = close SETTINGS;
182             }
183             }
184             else
185             {
186 0           $self->log->write('warning', 'Failed writing mcc timer settings in ['.$dir.']');
187             }
188              
189             }
190             }
191              
192             sub msg_status {
193 0     0 0   my($self, $index) = @_;
194              
195 0           $self->atsend('AT+MSR=0'.Device::Modem::CR);
196 0           my($ok, @data) = $self->parse_answer();
197 0 0         if( index($ok,'OK') >= 0 ) {
198 0           $self->log->write('info', 'MSR: '.join('/ ', @data));
199 0 0         return wantarray ? @data : join("\n", @data);
200             }
201             else
202             {
203 0           $self->log->write('warning', 'MSR: Error in querying status');
204 0           return undef;
205             }
206             }
207              
208             sub clear_memory
209             {
210 0     0 1   my($self, $memtype) = @_;
211 0 0         $memtype = 2 unless defined $memtype;
212 0           my $cmd = '';
213              
214 0 0 0       if( $memtype == 0 || $memtype eq 'all' )
    0 0        
    0 0        
215             {
216 0           $cmd = 'MEA';
217             }
218             elsif( $memtype == 1 || $memtype eq 'user' )
219             {
220 0           $cmd = 'MEU';
221             }
222             elsif( $memtype == 2 || $memtype eq 'messages' )
223             {
224 0           $cmd = 'MEM';
225             }
226              
227 0           $cmd = 'AT+' . $cmd . Device::Modem::CR;
228 0           $self->atsend($cmd);
229 0           $self->wait(500);
230 0           $self->log->write('info', 'cleared memory type '.$memtype);
231 0           return(1);
232             }
233              
234             sub fax_id_string
235             {
236 0     0 0   my $self = shift;
237 0           my $result = '';
238              
239 0 0         if( @_ )
240             {
241 0           $self->atsend( sprintf('AT+MFI="%s"',$_[0]) . Device::Modem::CR );
242 0           $self->wait(100);
243 0           my($ok, $ans) = $self->parse_answer();
244 0           $self->log->write('info', 'New Fax ID string set to ['.$_[0].']');
245 0           $result = $ok;
246             }
247             else
248             {
249             # Retrieve current fax id string
250 0           $self->atsend('AT+MFI?' . Device::Modem::CR);
251 0           $self->wait(100);
252 0           my($ok, $ans) = $self->parse_answer();
253 0           $self->log->write('info', 'Fax ID string is ['.$ans.']');
254             # Remove double quotes chars if present
255 0 0         $ans = substr($ans, 1, -1) if $ans =~ /^".*"$/;
256 0           $result = $ans;
257             }
258              
259 0           $self->log->write('debug', 'fax_id_string answer is ['.$result.']');
260 0           return($result);
261             }
262              
263             sub messages_info {
264 0     0 0   my $me = $_[0];
265 0           $me->atsend('AT+MSR=0'.Device::Modem::CR);
266 0           $me->wait(100);
267 0           my $info_string = $me->answer();
268 0           my @keys = qw(
269             memory_size memory_used stored_voice_msg unreleased_voice_msg
270             stored_fax_msg unreleased_fax_msg
271             );
272 0           my %data;
273 0           my $n = 0;
274 0           for( split ',', $info_string, 6 )
275             {
276 0           $data{$keys[$n++]} = 0 + $_;
277             }
278              
279 0           $me->log->write('info', "Memory size is $data{memory_size} Mb. Used $data{memory_used}%");
280 0           $me->log->write('info', "Voice messages: $data{unreleased_voice_msg}/$data{stored_voice_msg} unread");
281 0           $me->log->write('info', "Fax messages: $data{unreleased_fax_msg}/$data{stored_fax_msg} unread");
282              
283 0           return %data;
284             }
285              
286             sub message_dump {
287 0     0 0   my($me, $msg) = @_;
288 0           my %info = $me->message_info($msg);
289              
290 0 0 0       if( exists $info{index} && $info{index} > 0 )
291             {
292 0           $me->log->write('info', sprintf('message %d starts at page %d address %x%x', $msg, $info{page}, $info{addresshigh}, $info{addresslow}));
293 0           my $mem_page = $me->get_mem_page($info{page});
294 0           my $offset = $info{addresshigh} << 8 + $info{addresslow};
295 0           $me->log->write('info', sprintf('offset in page %d is %d (%x)', $info{page}, $offset, $offset));
296 0           $mem_page = substr($mem_page, $offset);
297 0           $me->message_scan_page($mem_page);
298             }
299              
300 0           return undef;
301             }
302              
303             sub message_scan_page($\$)
304             {
305 0     0 0   my($me, $page) = @_;
306              
307 0           my $block_len = 32768;
308 0           my $header_len = 32;
309 0           my $pos = 0;
310 0           my $len = length($page);
311              
312 0           while( $pos <= $len )
313             {
314             # Read next message
315             # XXX
316             #my $chksum = substr($page, $pos, 2);
317             #$pos += 2;
318 0           my $chksum = 0;
319              
320 0           my $block = substr($page, $pos, $block_len);
321 0           $pos += $block_len;
322              
323             # Check checksum
324 0           my $calc_chksum = 0;
325 0           for( 0 .. length($block) )
326             {
327 0           $calc_chksum += ord(substr($block,$_,1));
328 0           $calc_chksum &= 0xFF;
329             }
330              
331 0           my $header = substr($block, 0, $header_len);
332              
333 0           print "Calculated checksum = ", $calc_chksum, "\n";
334 0           print "Declared checksum = ", hex($chksum), "\n";
335              
336 0           my @msg = unpack('CCCCCCCCA20CSCS', $header);
337 0           my @fld = qw(index type info attrs recvstat days hours minutes sender p_page p_addr n_page n_addr);
338              
339 0           my %msg = map { $_ => shift(@msg) } @fld;
  0            
340              
341 0           foreach( @fld )
342             {
343 0           print "MESSAGGIO $_ = [", $msg{$_}, "]\n";
344             }
345 0           print "-" x 60, "\n";
346              
347             }
348              
349             }
350              
351             sub message_info {
352 0     0 0   my($me, $msg) = @_;
353              
354 0 0 0       unless( $msg > 0 && $msg < 255 )
355             {
356 0           $me->log->write('warning', 'message_info(): message index must be 0 < x < 255');
357 0           return undef;
358             }
359              
360             # Send message info command
361 0           $me->atsend("AT+MSR=$msg".Device::Modem::CR);
362 0           $me->wait(100);
363 0           my $info_string = $me->answer();
364 0           my @keys = qw(
365             index type information attributes status day hour minute
366             callerid page addresshigh addresslow checksum
367             );
368 0           my %data;
369 0           my $n = 0;
370 0           for( split(',', $info_string, scalar @keys) )
371             {
372 0           $data{$keys[$n]} = $_;
373 0           $me->log->write('info', 'Message '.$keys[$n].': '.$data{$keys[$n]});
374 0           $n++;
375             }
376              
377 0           return %data;
378             }
379              
380             sub _createSettingsDir {
381 0     0     my $self = $_[0];
382 0           my $ok = 1;
383 0           require File::Path;
384 0           my $dir = $self->_settingsDir();
385 0 0         if( ! -d $dir )
386             {
387 0           $ok = File::Path::mkpath( $dir, 0, 0700 );
388             }
389 0 0         return($ok ? $dir : undef);
390             }
391              
392             sub _settingsDir {
393 0     0     "$ENV{HOME}/.usrmodem"
394             }
395              
396             #
397             # retrieve and save a message in GSM format
398             #
399             sub extract_voice_message($)
400             {
401 0     0 0   my($self, $number) = @_;
402              
403 0           my $addr;
404             my $d;
405 0           my $data;
406 0           my $end;
407 0           my $header;
408 0           my $startpage;
409              
410             # Check if this message is really a voice message (type==2)
411 0           my %msg = $self->message_info($number);
412 0 0         return undef unless %msg;
413 0 0         return undef if $msg{type} != 2;
414              
415             # set startpage $stat($number.page)
416 0           $startpage = $msg{page};
417              
418             # Download the 1st page
419             #set d [GetMemPage $startpage]
420 0           $d = $self->get_mem_page($startpage);
421              
422             #set addr [expr $stat($number.hi)*256 + $stat($number.lo) + 2]
423 0           $addr = 2 + $msg{addresslow} + ($msg{addresshigh} << 8);
424              
425             #set header [string range $d $addr [expr $addr+34]]
426             #set data [string range $d [expr $addr+34] end]
427 0           $header = substr $d, $addr, 34; #$addr + 34;
428 0           $data = substr $d, $addr + 34;
429              
430             #warn('header ['.$header.']'.(length($header)));
431              
432             # Extract the data from the header
433             #binary scan $header cccccccca20cScS h_idx h_type h_info h_attr h_stat h_day h_hour h_min h_faxid h_ppage h_paddr h_npage h_naddr
434 0           my @hdr = unpack('cccccccca20cncn', $header);
435 0           my %hdr = map { $_ => shift @hdr } qw(idx type info attr stat day hour min faxid ppage paddr npage naddr);
  0            
436 0           undef @hdr;
437              
438             # set h_naddr [expr ($h_naddr + 0x10000) % 0x10000]
439             # set h_paddr [expr ($h_paddr + 0x10000) % 0x10000]
440             #$hdr{naddr} = ($hdr{naddr} + 0x10000) % 0x10000;
441             #$hdr{paddr} = ($hdr{paddr} + 0x10000) % 0x10000;
442 0           $hdr{naddr} &= 0xFFFF; #($hdr{naddr} + 0x10000) % 0x10000;
443 0           $hdr{paddr} &= 0xFFFF; #($hdr{paddr} + 0x10000) % 0x10000;
444              
445             #for (sort keys %hdr)
446             #{
447             # warn("header $_ {$hdr{$_}}");
448             #}
449              
450             # One or more pages ?
451 0 0         if($startpage == $hdr{npage})
452             {
453             # Only one page
454 0           $data = substr $data, 0, $hdr{naddr};
455             #warn('1page datalen:'.(length($data)));
456             }
457             else
458             {
459             # Get the following pages
460 0           $startpage++;
461 0           while( $startpage <= $hdr{npage} )
462             {
463             #set d [GetMemPage $startpage]
464 0           $d = $self->get_mem_page($startpage);
465              
466             # Remove the checksum
467 0 0         if( $hdr{npage} == $startpage )
468             {
469             # set end [expr $h_naddr - 1]
470 0           $end = $hdr{naddr} - 1;
471             }
472             else
473             {
474             #set end end
475             #$end = $end;
476             }
477              
478             # append data [string range $d 2 $end]
479 0 0         if( $end )
480             {
481 0           $data .= substr $d, 2, 2 + $end;
482             }
483             else
484             {
485 0           $data .= substr $d, 2;
486             }
487              
488             #warn('datalen:'.length($data));
489              
490             #incr startpage
491 0           $startpage++;
492             }
493             }
494              
495             # Unstuff data, $num should always be 1
496             # set pages(0) ""
497 0           my @pages = ();
498              
499             # set num [ByteUnstuff $data pages]
500 0           my $num = $self->_byte_unstuff($data, \@pages);
501              
502             # Gsm messages have always 1 page
503             #warn('length of final msg = '.length($pages[1]));
504 0           return $pages[1];
505             }
506              
507             sub _byte_stuff($)
508             {
509 0     0     my($self, $data) = @_;
510              
511             #
512             # Escape DLE (0x10) codes from data:
513             # DLE DLE <= DLE
514             # DLE SUB(0x1A) <= DLE DLE
515             # DLE ETX(0x03) = end of page
516             #
517             # I: data: data to decode
518             # R: escaped data
519             #
520             # set out ""
521 0           my $out = '';
522              
523 0           while (1)
524             {
525             # set id [string first "\x10" $data]
526 0           my $id = index($data, chr(0x10));
527             # if {$id == -1} break
528 0 0         last if $id == -1;
529              
530             #append out [string range $data 0 [expr $id - 1]]
531 0           $out .= substr($data, 0, $id - 1);
532              
533             #set nextchar [string index $data [expr $id+1]]
534 0           my $nextchar = substr($data, $id + 1, 1);
535              
536             #set data [string range $data [expr $id+2] end]
537 0           $data = substr($data, $id + 2);
538              
539             #switch $nextchar {
540             # "\x10" { append out \x10\x1A }
541             # default { append out \x10\x10$nextchar }
542             #}
543 0 0         if( $nextchar eq chr(0x10) )
544             {
545 0           $out .= chr(0x10) . chr(0x1A);
546             }
547             else
548             {
549 0           $out .= chr(0x10) . chr(0x10) . $nextchar;
550             }
551             }
552              
553             # add end of data
554             #append out $data\x10\x03
555 0           $out .= $data . chr(0x10) . chr(0x03);
556              
557 0           return $out;
558             }
559              
560             sub _byte_unstuff(@)
561             {
562              
563             #proc {ByteUnstuff} {data array} {
564             #
565             # Unescape DLE (0x10) codes from data:
566             # DLE DLE => DLE
567             # DLE SUB(0x1A) => DLE DLE
568             # DLE ETX(0x03) = end of page, the data is put in another hash
569             #
570             # I: data: data to decode
571             # O: array: contains one or more pages of data (array(1), array(2)...)
572             # R: number of pages
573             #
574              
575 0     0     my($self, $data, $r_pages) = @_;
576 0   0       $r_pages ||= [];
577              
578 0           my $numpage = 1;
579 0           my $out = '';
580 0           my $id;
581              
582 0           while (1)
583             {
584             # set id [string first "\x10" $data]
585 0           $id = index($data, DLE);
586 0 0         last if $id == -1;
587              
588             #append out [string range $data 0 [expr $id - 1]]
589 0           $out .= substr($data, 0, $id);
590              
591             #set nextchar [string index $data [expr $id+1]]
592 0           my $nextchar = substr($data, $id + 1, 1);
593             #set data [string range $data [expr $id+2] end]
594 0           $data = substr($data, $id + 2);
595              
596             #switch $nextchar {
597             #"\x10" { append out \x10 }
598             #"\x1A" { append out \x10\x10 }
599             #"\x03" { set adata($numpage) $out
600             # set out ""
601             # incr numpage
602             # # end of page
603             #}
604             #default { append out \x10$nextchar }
605             #}
606 0 0         if( $nextchar eq DLE )
    0          
    0          
607             {
608 0           $out .= DLE;
609             }
610             elsif( $nextchar eq SUB )
611             {
612 0           $out .= DLE . DLE;
613             }
614             elsif( $nextchar eq ETX )
615             {
616 0           $r_pages->[$numpage++] = $out;
617 0           $out = '';
618             }
619             else
620             {
621 0           $out .= DLE . $nextchar;
622             }
623              
624             }
625              
626             # Manage last page
627 0           $r_pages->[$numpage] = $out . $data;
628              
629 0           return $numpage;
630             }
631              
632              
633             1;
634              
635             =head1 NAME
636              
637             Device::Modem::UsRobotics - USR modems extensions to control self-mode
638              
639             =head1 SYNOPSIS
640              
641             use Device::Modem::UsRobotics;
642              
643             my $modem = Device::Modem::UsRobotics->new( port => '/dev/ttyS1' );
644             $modem->connect( baudrate => 9600 );
645             my %info = $modem->messages_info();
646             print "There are $info{unreleased_voice_msg} unread voice messages on $info{stored_voice_msg} total\n";
647             print "There are $info{unreleased_fax_msg} unread fax messages on $info{stored_fax_msg} total\n";
648              
649             # Get details about message n. X
650             my %msg = $modem->message_info(1);
651             index type information attributes status day hour minute
652             callerid page addresshigh addresslow checksum
653             print 'This is a ', ($msg{type} == 2 ? 'voice' : 'fax'), 'message', "\n";
654             print 'It came from no. ', $msg{callerid}, "\n";
655             # ...
656              
657             # Now clear all messages
658             $modem->clear_memory();
659              
660             =head1 WARNING
661              
662             This module is not documented yet, and it is a rough work in progress.
663             Until now, it correctly reads voice/fax messages information, but when
664             saving voice messages to disk, sometimes they are incorrectly decoded.
665              
666             So, if you need a working program, check out the good old TkUsr by
667             Ludovic Drolez, unless you want to help develop Device::Modem::UsRobotics.
668              
669             =head1 DOCS TO BE COMPLETED FROM NOW.....
670              
671             Yes, I'm a bad boy :-)
672              
673             =head1 DESCRIPTION
674              
675             Bla Bla Bla...
676              
677             =head1 METHODS
678              
679             =head2 clear_memory()
680              
681             Used to permanently clear the memory space of the modem. There are separate memory
682             spaces, one for voice/fax messages and one for user settings. Examples:
683              
684             $modem->clear_memory('user'); # or $modem->clear_memory(1)
685             $modem->clear_memory('messages'); # or $modem->clear_memory(2)
686              
687             To clear both, you can use:
688              
689             $modem->clear_memory('all'); # or $modem->clear_memory(0);
690              
691             Parameters:
692              
693             =over 4
694              
695             =item C<$memtype>
696              
697             String or integer that selects the type of memory to be cleared,
698             where C<0> is for C, C<1> is for C memory, C<2> is for C
699             memory.
700              
701             =back
702              
703              
704             =head1 SUPPORT
705              
706             Please feel free to contact me at my e-mail address L
707             for any information, to resolve problems you can encounter with this module
708             or for any kind of commercial support you may need.
709              
710             =head1 AUTHOR
711              
712             Cosimo Streppone, L
713              
714             =head1 COPYRIGHT
715              
716             (C) 2004-2005 Cosimo Streppone, L
717              
718             This library is free software; you can only redistribute it and/or
719             modify it under the same terms as Perl itself.
720              
721             =head1 SEE ALSO
722              
723             Device::Modem,
724             perl
725              
726             =cut
727              
728             # vim: set ts=4 sw=4 tw=0 nowrap nu