:pserver:anonymous@intra.tektonica.com:/opt/cvs - gsmperl - Fri Dec 21 07:37 CET...
[gsmperl.git] / GSM / SMS / Transport / Serial.pm
1 package GSM::SMS::Transport::Serial;
2
3 #
4 # SMS transport layer for serial GSM modems
5 #
6
7 use GSM::SMS::Transport::Transport;
8 @ISA = qw(GSM::SMS::Transport::Transport);
9
10 $VERSION = '0.1';
11
12 use Device::SerialPort;
13 use GSM::SMS::Log;
14 use GSM::SMS::Spool;
15
16 # All the parameters I need to run ...
17 my @config_vars = qw( 
18         name
19         match   
20         serial-port
21         baud-rate
22         heartbeat
23         pin-code
24         csca
25         spoolout
26         spoolin
27                                         );
28
29 my $__TO = 200;
30
31 # constructor
32 sub new {
33         my $proto = shift;
34         my $class = ref($proto) || $proto;
35
36         my $self = {};
37         $self->{cfg} = shift;
38         
39         $self->{'__LOGGER__'} = GSM::SMS::Log->new( $self->{cfg}->{"log"} );
40         
41         bless($self, $class);
42         my $success = $self->init( $self->{cfg} );
43         return undef if $success == -1;
44         return $self;
45
46
47 # Send a PDU encoded message
48 sub send {
49         my($self, $msisdn, $p) = @_;
50         my $logger = $self->{'__LOGGER__'};
51         chomp($p);
52
53         # print "MSISDN : $msisdn\n";
54         # print "MSG: $p\n";
55
56         # calculate length of message
57         my $len =  length($p)/2 - 1;
58         $len-=hex( substr($p, 0, 2) );
59   
60         # $self->_at("ATE0\r", $__TO);
61     $self->_at("AT+CMGF=0\r", $__TO);
62     $self->_at("AT+CMGS=$len\r", $__TO, ">");
63     my $res = $self->_at("$p\cz", $__TO);
64
65         # print "## $res\n"; 
66         
67         if ($res=~/OK/) {
68                 $logger->logentry("send [$p]") if $logger;
69                 return 0;
70         } else {
71                 $logger->logentry("error sending [$p]") if $logger;
72                 return -1;
73         }
74 }
75
76
77 # Receive a PDU encoded message
78 # Will return a PDU string in $pduref from the modem IF we have a message pending 
79 # return
80 #       0 if PDU received
81 #       -1 if no message pending
82 sub receive {
83         my ($self, $pduref) = @_;
84
85         my $ar = $self->{MSGARRAY};
86         my $msg = shift (@$ar);         # shift because we want to delete lower index first (problem with modem)
87         if (!$msg) {
88                 
89                 # Read in pending messages
90                 my $msgarr = $self->_getSMS();
91                 foreach my $msg (@$msgarr) {
92                         push @$ar, $msg;
93                 }
94                 $msg = shift (@$ar);    # shift same reason as above
95         }
96         if ($msg) {
97                 $$pduref = $msg->{MSG};
98                 if ($msg->{LENGTH}) {
99                         $self->_delete($msg->{ID});
100                 }
101                 return 0;
102         }
103         return -1;
104 }
105
106
107 # Initialise this transport layer$
108 #  No init file -> default initfile for transport
109 sub init {
110         my ($self, $config) = @_;
111
112         my $ress = $self->_init($config);
113         if ( $ress ) {
114                 return -1;
115         }
116
117         $self->{MSGARRAY} = [];
118         
119         return 0;
120 }
121
122
123
124 # Close the init file
125 sub close {
126         my ($self) =@_;
127
128         my $logger = $self->{'__LOGGER__'};
129         
130         my $l = $self->{log};
131         undef $self->{port};
132         $logger->logentry("Serialtransport stopped") if $logger;
133 }
134
135
136 # A ping command .. just return an informative string on success
137 sub ping {
138         my ($self) = @_;
139
140         return $self->_getSQ();
141 }
142
143 # give out the needed config paramters
144 sub get_config_parameters {
145         my ($self) = @_;
146
147         return @config_vars;
148 }
149
150 # Do we have a valid route for this msisdn
151 sub has_valid_route {
152         my ($self, $msisdn) = @_;
153
154         # print "in route\n";
155         # print Dumper $self->{cfg};
156         foreach my $route ( split /,/, $self->{cfg}->{"match"} ) {
157                 # print $route;
158                 return -1 if $msisdn =~ /$route/;
159         }
160         return 0;
161 }
162
163 sub get_name {
164         my ($self) = @_;
165
166         return $self->{cfg}->{name};
167 }
168
169 ###############################################################################
170 # Transport layer specific functions
171 #
172 sub _init {
173         my ($self, $cfg) = @_;
174
175         my $logger = $self->{'__LOGGER__'};
176         
177         # print Dumper $cfg;
178
179
180         # Start of log ...
181         $logger->logentry('Starting Serial Transport for '.$cfg->{"name"}) if $logger;
182         
183         # Get configuration from config file
184
185         
186
187         $self->{cfg} = $cfg;
188
189         my $port = $cfg->{"serial-port"}        ||      return -1;
190         my $br   = $cfg->{"baud-rate"}          ||      return -1;
191         my $pc   = $cfg->{"pin-code"}           ||      return -1;
192         my $csca = $cfg->{"csca"}               ||      return -1;
193         my $modem = $cfg->{"name"};
194         
195         # Start up serial port
196         $self->{port} = Device::SerialPort->new ($port);
197         $self->{port}->baudrate($br);
198         $self->{port}->parity("none");
199         $self->{port}->databits(8);
200         $self->{port}->stopbits(1);
201
202         # Try to communicate to the port
203         $self->_at("ATZ\r", $__TO);
204         $self->_at("ATE0\r", $__TO);
205         my $res = $self->_at("AT\r", $__TO);
206
207         unless ($res =~/OK/is) {
208                 $logger->logentry('Could not communicate to '.$port.'.') if $logger;
209                 return -1;
210         }
211
212         # Check the modem status (PIN, CSCA and network connection)
213         return -1 unless ( $self->_register );
214
215         $logger->logentry("Modem is alive! (SQ=".$self->_getSQ()."dBm)") if $logger;
216         return 0;
217 }
218
219
220
221 sub _getSMS {
222         my ($self) = @_;
223         my $result = [];
224         my $msgcount=0;
225
226         # to pdu mode
227         $self->_at("AT+CMGF=0\r", $__TO);
228
229         # loop from 1 to cfg->memorylimit to get messages
230         my $limit = $self->{cfg}->{"memorylimit"} || 10;
231         for (my $i=1; $i<=$limit; $i++) {
232                 my $res = $self->_at( "AT+CMGR=$i\r", $__TO );
233
234                 next if ($res=~/ERROR/ig);
235
236                 # find +CMGR: ..,..,..
237                 my $cmgr_start  = index( $res, "+CMGR:" );
238                 my $cmgr_stop   = index( $res, "\r", $cmgr_start );
239                 my $cmgr = substr($res, $cmgr_start, $cmgr_stop - $cmgr_start);
240
241                 # find PDU string
242                 my $pdu_start   = $cmgr_stop + 2;
243                 my $pdu_stop    = index( $res, "\r", $pdu_start );
244                 my $pdu  = substr($res, $pdu_start, $pdu_stop - $pdu_start);
245
246                 # message settings
247                 $cmgr=~/\+CMGR:\s+(\d*),(\d*),(\d*)/;
248
249                 my $msg = $result->[$msgcount++] = {};
250                 $msg->{'ID'} = $i;
251                 $msg->{'STAT'} = $1;
252                 $msg->{'LENGTH'} = $3;
253                 $msg->{'MSG'}.=$pdu;
254         }
255         return $result;
256 }
257
258
259 sub _delete {
260         my ($self, $id) = @_;
261         
262         my $l=$self->{log};
263
264         $self->_at("AT+CMGF=1\r", $__TO);
265         my $res = $self->_at("AT+CMGD=".$id."\r", $__TO);
266         # my $res="OK"; 
267
268         # print "DELETE $res\n";
269
270         if ($res=~/OK/) {
271         } else {
272                 exit(0);
273         }
274 }
275
276
277 sub _at {
278     my ($self, $at, $timeout, $expect) = @_;
279  
280         my $ob = $self->{port};
281
282     $ob->purge_all;
283     $ob->write("$at");
284     $ob->write_drain;
285  
286     my $in = "";
287     my $max = 500;
288     my $count = 0;
289     my $found = 0;
290     my $to_read;
291     my $readcount;
292     my $input;
293     my $counter=0;
294
295         # print "$at\n";
296  
297     do {
298         select(undef,undef,undef, 0.1);
299         $to_read = $max - $count;
300  
301         ($readcount, $input) = $ob->read($to_read);
302         $in.=$input;
303         $count+=$readcount;
304  
305         if ( ($in=~/OK\r\n/) || ($in=~/ERROR\r\n/) ) {
306             $found=1;
307         }
308  
309         if ( $expect ne "" ) {
310             if ( index($in, $expect) > -1 ) {
311                 $found=1;
312             }
313         }
314         $counter++;
315  
316  
317     } while ( ($found==0) && ($counter<$timeout) );
318  
319     select(undef,undef,undef, 0.1);
320  
321     $to_read=$max-$count;
322     ($readcount, $input) = $ob->read($to_read);
323  
324     $in.=$input;
325
326         # print "# $in #\n";
327
328     return $in;
329 }                                                                                          
330
331 sub _getSQ {
332         my ($self) = @_;
333         my $res = $self->_at("AT+CSQ\r", $__TO);
334         $res=~/\+CSQ:\s+(\d+),(\d+)/igs;
335         $res=$1;
336         
337         my $dbm;
338
339         # transform into dBm
340         $dbm = -113 if ($res == 0);
341         $dbm = -111 if ($res == 1);
342         $dbm = -109 + 2*($res-2) if (($res >= 2) && ($res <=30));
343         $dbm = 0 if ($res == 99); 
344
345         return $dbm;
346 }
347
348 # Register modem: PIN, CSCA, Wait for network connectivity for a certain period
349 sub _register {
350         my ($self) = @_;
351         my $res;
352
353         my $logger = $self->{'__LOGGER__'};
354         
355         my $cfg = $self->{cfg};
356
357         my $pc   = $cfg->{"pin-code"};
358         my $csca = $cfg->{"csca"};      
359         
360     $logger->logentry( "Checking if modem ready .." ) if $logger;    
361
362         # I should speed this 'registering' up ...
363         # ... actually I should have a command that tells me
364         # ready to send|receive ...
365         #
366         # $res = $self->_at("AT+MAGIC?\r", $__TO);
367         # return -1 if ( $res=~/OK/ );
368         
369         # 1. Do we need to give in the PIN ?
370         $res = $self->_at("AT+CPIN?\r", $__TO, "+CPIN:");
371
372         if ( $res=~/\+CPIN: SIM PIN/i ) {
373                 # Put PIN
374                 $logger->logentry("Modem needs PIN ...") if $logger;    
375                 $self->_at("AT+CPIN=\"$pc\"\r", $__TO);
376                  
377                 # Check PIN
378                 $res = $self->_at("AT+CPIN?\r", $__TO , "+CPIN:");
379                 if( $res!~/\+CPIN: READY/i ) {
380                         # somethings wrong here!
381                         $logger->logentry("Modem did not accept PIN!") if $logger;
382                         return 0;
383                 }
384         }
385
386         # 2. Set the CSCA
387         $res = $self->_at("AT+CSCA=\"$csca\"\r", $__TO);
388         $res = $self->_at("AT+CSCA=\"$csca\"\r", $__TO);
389
390         # 3. Wait for registration on network
391         my $registered = 0;
392         my $stime = time;
393         do {
394                 $res = $self->_at("AT+CREG?\r", $__TO , "+CREG");
395                 if ( $res=~/1/i ) {
396                         $registered++;
397                 }
398         }
399         until ( $registered || ((time - $stime) > 10 ) );
400
401         if( $registered==0 ) {
402                 $logger->logentry("Modem could not register on network!") if $logger;
403                 return 0;
404         }
405
406         # 4. Wait until SIM chip is ready (give it 3 mins) - by checking +cmgf
407     my $simReady = 0;
408     $stime = time;
409     do {
410         $res = $self->_at("AT+CMGF=0\r", $__TO , "+CMGF");
411         if ( $res=~/OK/i ) {
412             $simReady++;
413         } else {
414             sleep 1;
415         }
416     }
417     until ( $simReady || ((time - $stime) > 180 ) );
418
419     if( $simReady==0 ) {
420         $logger->logentry("SIM will not respond!") if $logger;
421         return 0;
422     }
423
424         # All went fine!
425
426         return -1;
427 }
428 1;
429
430 =head1 NAME
431
432 GSM::SMS::Transport::Serial
433
434 =head1 DESCRIPTION
435
436 This class implements a serial transport. It uses Device::SerialPort to communicate to the modem. At the moment the modem that I recommend is the WAVECOM modem. This module is in fact the root of the complete package, as the project started as a simple perl script that talked to a Nokia6110 connected via a software modem ( as that model did not implement standard AT ) on a WINNT machine, using Win32::SerialPort and Activestate perl. Also tested with the M20 modem module from SIEMENS. 
437
438 I first used the Nokia6110, then moved to the Falcom A1 GSM modem, then moved to the SIEMENS M20 and then moved to the WAVECOM series. Both M20 and WAVECOM worked best, but I could crash the firmware in the M20 by sending some fake PDU messages.
439
440 =head1 ISSUES
441
442 The Device::SerialPort puts a big load on your system (active polling).
443
444 The initialisation does not always work well and sometimes you have to
445 initialize your modem manually using minicom or something like that.
446
447         >minicom -s
448         AT
449         AT+CPIN?
450         AT+CPIN="nnn"
451         AT+CSCA?
452         AT+CSCA="+32475161616"
453
454 +CPIN puts the pin-code in the modem; Be carefull, only 3 tries and then you have to provide the PUK code etc ...
455
456 +CSCA sets the service center address
457
458 =head1 AUTHOR
459
460 Johan Van den Brande <johan@vandenbrande.com>