File Coverage

blib/lib/Device/Gsm/Pdu.pm
Criterion Covered Total %
statement 63 98 64.2
branch 12 26 46.1
condition 2 3 66.6
subroutine 10 15 66.6
pod 5 12 41.6
total 92 154 59.7


line stmt bran cond sub pod time code
1             # Device::Gsm::Pdu - PDU encoding/decoding functions for Device::Gsm class
2             # Copyright (C) 2002-2011 Cosimo Streppone, cosimo@cpan.org
3             # Copyright (C) 2006-2011 Grzegorz Wozniak, wozniakg@gmail.com
4             #
5             # This program is free software; you can redistribute it and/or modify
6             # it only under the terms of Perl itself.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # Perl licensing terms for details.
12             #
13             # Commercial support is available. Write me if you are
14             # interested in new features or software support.
15             #
16             # $Id$
17              
18             # TODO document decode_text8()
19              
20             package Device::Gsm::Pdu;
21              
22 10     10   205488 use strict;
  10         18  
  10         382  
23 10     10   7912 use Device::Gsm::Charset;
  10         26  
  10         322  
24 10     10   7900 use Device::Gsm::Sms::Token::UDH;
  10         30  
  10         16001  
25              
26             # decode a pdu encoded phone number into human readable format
27             sub decode_address {
28 12 50   12 1 490 my $address = shift or return;
29              
30 12         18 my $number;
31 12         62 my ($length, $type, $bcd_digits) = unpack('A2 A2 A*', $address);
32              
33             # XXX DEBUG
34             #print STDERR "len=$length type=$type bcd=$bcd_digits\n";
35              
36             # Manage alphabetical addresses (as per TS 03.38 specs)
37             # Alphabetical addresses begin with 'D0'.
38             # Check also http://smslink.sourceforge.net/pdu.html
39             #
40 12 100       40 if ($type eq 'D0') {
41 3         22 $number = decode_text7($length . $bcd_digits);
42 3         16 return $number;
43             }
44              
45             # Reverse each pair of bcd digits
46 9         23 while ($bcd_digits) {
47 48         74 $number .= reverse substr($bcd_digits, 0, 2);
48 48         132 $bcd_digits = substr $bcd_digits, 2;
49             }
50              
51             #print STDERR "num=$number - ";
52              
53             # Truncate last `F' if found (XXX ???)
54             #$number = substr( $number, 0, hex($length) );
55 9 100       35 chop $number if substr($number, -1) eq 'F';
56              
57             # Decode special characters for GPRS dialing
58 9         21 $number =~ s/A/\*/;
59 9         14 $number =~ s/B/#/;
60              
61             # If number is international, put a '+' sign before
62 9 100 66     62 if ($type == 91 && $number !~ /^\s*\+/) {
63 5         14 $number = '+' . $number;
64             }
65              
66 9         39 return $number;
67             }
68              
69             sub decode_text7 {
70 21     21 0 803 pack '(b*)*',
71             unpack 'C/(a7)',
72             pack 'C a*',
73             unpack 'C b*',
74             pack 'H*', $_[0];
75             }
76              
77             #remains for comatibility reasons with my production scripts :)
78             sub decode_text7_udh1 {
79 0     0 0 0 my $unpacked = join '',
80             unpack 'C/(a7)',
81             pack 'C a*',
82             unpack 'C b*',
83             pack 'H*', $_[0];
84              
85             #remove bit of padding from message
86 0         0 $unpacked = substr($unpacked, 1, length($unpacked));
87 0         0 pack '(b*)*', ($unpacked =~ m/([01]{1,7})/gs);
88             }
89              
90             #decode text with padding
91             sub decode_text7_udh {
92 0     0 0 0 my ($encoded, $padding) = @_;
93 0 0       0 $padding = 0 unless ($padding);
94 0         0 my $unpacked = join '',
95             unpack 'C/(a7)',
96             pack 'C a*',
97             unpack 'C b*',
98             pack 'H*', $encoded;
99              
100             #remove bits of padding from message
101 0         0 $unpacked = substr($unpacked, $padding, length($unpacked));
102 0         0 pack '(b*)*', ($unpacked =~ m/([01]{7})/gs);
103              
104             }
105              
106             # decode 8-bit encoded text
107             sub decode_text8($) {
108              
109 0     0 0 0 my $text8 = shift();
110 0 0       0 return unless $text8;
111              
112 0         0 my $str;
113 0         0 while ($text8) {
114 0         0 $str .= chr(hex(substr $text8, 0, 2));
115 0 0       0 if (length($text8) > 2) {
116 0         0 $text8 = substr($text8, 2);
117             }
118             else {
119 0         0 $text8 = '';
120             }
121             }
122 0         0 return $str;
123             }
124              
125             sub encode_address {
126 5     5 1 18 my $num = shift;
127 5         7 my $type = '';
128 5         7 my $len = 0;
129 5         7 my $encoded = '';
130              
131 5         11 $num =~ s/\s+//g;
132              
133             #warn('encode_address('.$num.')');
134              
135             # Check for alphabetical addresses (TS 03.38)
136 5 50       16 if ($num =~ /[A-Z][a-z]/) {
137              
138             # Encode clear text in gsm0338 7-bit
139 0         0 $type = 'D0';
140 0         0 $encoded = encode_text7($num);
141 0         0 $len = unpack 'H2' => chr(length $encoded);
142             }
143             else {
144 5 100       20 $type = index($num, '+') == 0 ? 91 : 81;
145              
146             # Remove all non-numbers. Beware to GPRS dialing chars.
147 5         18 $num =~ s/[^\d\*#]//g;
148 5         7 $num =~ s/\*/A/g; # "*" maps to A
149 5         9 $num =~ s/#/B/g; # "#" maps to B
150              
151 5         21 $len = unpack 'H2' => chr(length $num);
152 5         10 $num .= 'F';
153 5         27 my @digit = split // => $num;
154              
155 5         17 while (@digit > 1) {
156 28         70 $encoded .= join '', reverse splice @digit, 0, 2;
157             }
158             }
159              
160             #warn(' [' . (uc $len . $type . $encoded ) . ']' );
161              
162 5         26 return (uc $len . $type . $encoded);
163             }
164              
165             sub decode_text_UCS2 {
166 5     5 0 18 my $encoded = shift;
167 5 50       14 return undef unless $encoded;
168              
169 5         12 my $len = hex substr($encoded, 0, 2);
170 5         8 $encoded = substr $encoded, 2;
171              
172 5         8 my $decoded = "";
173 5         15 while ($encoded) {
174 5         20 $decoded .= pack("C0U", hex(substr($encoded, 0, 4)));
175 5         15 $encoded = substr($encoded, 4);
176             }
177 5         22 return $decoded;
178             }
179              
180             sub encode_text7 {
181             uc
182 12     12 1 3694 unpack 'H*',
183             pack 'C b*',
184             length $_[0],
185             join '',
186             unpack '(b7)*', $_[0];
187             }
188              
189             #
190             #return complete ud with udh
191             #remains for comatibility reasons with my production scripts :)
192             #
193             sub encode_text7_udh1 {
194 0     0 0 0 my $decoded = shift;
195 0         0 my $udh1 = shift;
196 0         0 my $decoded_length = length($decoded);
197 0         0 $decoded = Device::Gsm::Charset::iso8859_to_gsm0338($decoded);
198 0         0 my $pdu_msg = uc
199             unpack 'H*',
200             pack 'b*',
201              
202             #add one bit of padding to align septet boundary
203             '0' . join '', unpack '(b7)*', $decoded;
204              
205             #below add 7 septets length for udh1
206             return
207 0         0 sprintf("%02X", $decoded_length + Sms::Token::UDH::UDH1_LENGTH)
208             . $udh1
209             . $pdu_msg;
210             }
211              
212             #
213             #encode text with padding
214             #
215             sub encode_text7_udh {
216 0     0 0 0 my $decoded = shift;
217 0         0 my $padding = shift;
218 0 0       0 $padding = 0 unless ($padding);
219 0         0 my $decoded_length = length($decoded);
220 0         0 $decoded = Device::Gsm::Charset::iso8859_to_gsm0338($decoded);
221 0         0 my $pdu_msg = uc
222             unpack 'H*',
223             pack 'b*',
224              
225             #add bits of padding to align septet boundary
226             '0' x $padding . join '', unpack '(b7)*', $decoded;
227              
228             #below add septets length of text
229 0         0 my $len_hex = sprintf("%02X", $decoded_length);
230             return
231             wantarray
232 0 0       0 ? ($len_hex, $pdu_msg, $len_hex . $pdu_msg)
233             : $len_hex . $pdu_msg;
234             }
235              
236             sub pdu_to_latin1 {
237              
238             # Reattach a length octet.
239 1     1 1 12 my $s = shift;
240 1         5 my $len = length $s;
241              
242             #arn "len=$len, len/2=", $len/2, "\n";
243 1         13 my $l = uc unpack("H*", pack("C", int(length($s) / 2 * 8 / 7)));
244 1 50       7 if (length($l) % 2 == 1) { $l = '0' . $l }
  0         0  
245 1         7 my $pdu = $l . $s;
246              
247             #arn "l=$l, pdu=$pdu\n";
248 1         5 my $decoded = Device::Gsm::Pdu::decode_text7($pdu);
249              
250             #arn "decoded_text7=$decoded\n";
251 1         16 my $latin1 = Device::Gsm::Charset::gsm0338_to_iso8859($decoded);
252              
253             #arn "latin1=$latin1\n";
254 1         14 return $latin1;
255             }
256              
257             sub latin1_to_pdu {
258 1     1 1 32 my $latin1_text = $_[0];
259              
260             #arn "latin1=$latin1_text\n";
261 1         6 my $gsm0338 = Device::Gsm::Charset::iso8859_to_gsm0338($latin1_text);
262              
263             #arn "gsm0338=$gsm0338\n";
264 1         5 my $fullpdu = Device::Gsm::Pdu::encode_text7($gsm0338);
265              
266             #arn "pdu=$fullpdu\n";
267 1         16 return substr($fullpdu, 2); # strip off the length octet
268             }
269              
270             1;
271              
272             =head1 NAME
273              
274             Device::Gsm::Pdu - library to manage PDU encoded data for GSM messaging
275              
276             =head1 WARNING
277              
278             This is C software, still needs extensive testing and
279             support for custom GSM commands, so use it at your own risk,
280             and without C warranty! Have fun.
281              
282             =head1 NOTICE
283              
284             This module is meant to be used internally by C class,
285             so you probably do not want to use it directly.
286              
287             =head1 SYNOPSIS
288              
289             use Device::Gsm::Pdu;
290              
291             # DA is destination address
292             $DA = Device::Gsm::Pdu::encode_address('+39347101010');
293             $number = Device::Gsm::Pdu::decode_address( $DA );
294              
295             # Encode 7 bit text to send messages
296             $text = Device::Gsm::Pdu::encode_text7('hello');
297              
298             =head1 DESCRIPTION
299              
300             C module includes a few basic functions to deal with SMS in PDU mode,
301             such as encoding GSM addresses (phone numbers) and, for now only, 7 bit text.
302              
303             =head1 FUNCTIONS
304              
305             =head2 decode_address( pdu_encoded_address )
306              
307             Takes a PDU encoded address and decodes into human-readable mobile number.
308             If number type is international, result will be prepended with a `+' sign.
309              
310             Clearly, it is intended as an internal function.
311              
312             =head3 Example
313              
314             print Device::Gsm::Pdu::decode_address( '0B919343171010F0' );
315             # prints `+39347101010';
316              
317             =head2 encode_address( mobile_number )
318              
319             Takes a mobile number and encodes it as DA (destination address).
320             If it begins with a `+', as in `+39328101010', it is treated as an international
321             number.
322              
323             =head3 Example
324              
325             print Device::Gsm::Pdu::encode_address( '+39347101010' );
326             # prints `0B919343171010F0'
327              
328             =head2 encode_text7( text_string )
329              
330             Encodes some text ASCII string in 7 bits PDU format, including a header byte
331             which tells the length is septets. This is the only 100% supported mode to
332             encode text.
333              
334             =head3 Example
335              
336             print Device::Gsm::Pdu::encode_text7( 'hellohello' );
337             # prints `0AE832...'
338              
339             =head2 pdu_to_latin1($pdu)
340              
341             Converts a PDU (without the initial length octet) into a latin1 string.
342              
343             =head3 Example
344              
345             my $pdu = 'CAFA9C0E0ABBDF7474590E8296E56C103A3C5E97E5';
346             print Device::Gsm::Pdu::pdu_to_latin1($pdu);
347             # prints `Just another Perl hacker'
348              
349             =head2 latin1_to_pdu($text)
350              
351             Converts a text string in latin1 encoding (ISO-8859-1) into a PDU string.
352              
353             =head3 Example
354              
355             my $text = "Just another Perl hacker";
356             print Device::Gsm::Pdu::latin1_to_pdu($text);
357             # prints `CAFA9C0E0ABBDF7474590E8296E56C103A3C5E97E5'
358              
359             =head1 AUTHOR
360              
361             Cosimo Streppone, cosimo@cpan.org
362              
363             =head1 COPYRIGHT
364              
365             This library is free software; you can redistribute it and/or modify
366             it only under the terms of Perl itself.
367              
368             =head1 SEE ALSO
369              
370             Device::Gsm(3), perl(1)
371