7ece53ad06b6f08cc8683d983b26ce9e910763da
[gsmperl.git] / GSM / SMS / NBS / Message.pm
1 package GSM::SMS::NBS::Message;
2
3 use strict;
4 use warnings;
5
6 use     GSM::SMS::NBS::Lib;
7 use     GSM::SMS::PDU;
8 use Carp;
9
10 use vars qw($VERSION);
11 $VERSION = '0.1';
12
13 use constant DEFAULT_DCS_UDH    => 0xF5; # 8bit, ME specific
14 use constant DEFAULT_DCS_NOUDH  => 0x00; # default DCS (typically 7bit, SIM specific)
15 use constant DEFAULT_VALIDITY   => "1d"; # FIXME: currently fixed to 1 day
16 use constant DEFAULT_SMS_CENTER => "";   # FIXME: currently fixed to empty SMS center address
17
18 use constant USERDATA_LENGTH    => 140;
19 use constant DECONCAT_TOTAL     => "_deconcat_total";  # internal: undef=>try single message, *=>try & messages
20 use constant TEXT_POSITION      => "text_position";    # numeric position in $payload, UDH IE will be present only once
21
22 use constant IEI_ANIMATION_LARGE  => 0x0E;  #IEI: EMS large animation (16x16 x4 = 128 bytes)
23 use constant IEI_ANIMATION_SMALL  => 0x0F;  #IEI: EMS small animation ( 8x 8 x4 =  32 bytes)
24 use constant IEI_PICTURE_LARGE    => 0x10;  #IEI: EMS large    picture (32x32 = 128 bytes)
25 use constant IEI_PICTURE_SMALL    => 0x11;  #IEI: EMS small    picture (16x16 =  32 bytes)
26 use constant IEI_PICTURE_VARIABLE => 0x12;  #IEI: EMS variable picture (other)
27 use constant IEI_ALCATEL          => 0x80;  #IEI: Alcatel proprietary "data download" type
28
29 # SAR for NBS messages
30 # --------------------
31 # This part does the segmentation ... look in Stack.pm for reassembly
32
33 sub new {
34         my $proto = shift;
35         my $class = ref($proto) || $proto;
36
37         my $self = {};
38         $self->{'__FRAMES__'} = [];
39
40         bless($self, $class);
41         return $self;
42 }
43
44 # Create a message from a payload -- compatibility cludge!
45 sub create {
46         my ($self, $number, $payload, $destination_port, $source_port, $datacodingscheme) = @_;
47         my %dcs_compat = (
48                         "7bit" =>0x00,  # default DCS (typically 7bit, SIM specific)
49                         "7biti"=>0xF0,  # 7bit, immediate display
50                         "8bit" =>0xF6,  # 8bit, SIM specific
51                         "8biti"=>0xF4,  # 8bit, immediate display
52                         "8bitm"=>0xF5,  # 8bit, ME specific
53         );
54
55         $datacodingscheme=$dcs_compat{$datacodingscheme} if $datacodingscheme && exists $dcs_compat{$datacodingscheme};
56
57         return $self->store($number, pack("H*", $payload),
58                         "dcs"=>$datacodingscheme,
59                         "udh"=>[
60                                 {
61                                         "type"            =>"port16",  #IEI: application port addressing scheme, 16 bit address
62                                         "destination_port"=>$destination_port,
63                                         "source_port"     =>$source_port,
64                                 },
65                         ],
66                         );
67 }
68
69 sub _iebuilder {
70         my ($self, $iei, @iedata) = @_;
71
72         return ($iei,scalar(@iedata),@iedata);
73 }
74
75 sub _ie_port16 {
76         my ($self, %args) = @_;
77
78         return () if !defined $args{"destination_port"};
79         my $source_port=$args{"source_port"} || $args{"destination_port"};
80
81         return $self->_iebuilder(0x05,  #IEI: application port addressing scheme, 16 bit address
82                         ($args{"destination_port"} >> 8), ($args{"destination_port"} & 0xFF),
83                         ($source_port >> 8), ($source_port & 0xFF),
84         );
85 }
86
87 sub _ie_concat8 {
88         my ($self, %args) = @_;
89
90         return $self->_iebuilder(0x00,  #IEI: concatenated short message, 8 bit reference
91                         $args{"drn"},  # reference number
92                         $args{"fmax"}, # maximum number of short messages
93                         $args{"fsn"},  # sequence number of the current short message
94         );
95 }
96
97 # Use $args{"force_variable"} to force type IEI_PICTURE_VARIABLE
98 # It needs to be used for compatibility with Alcatel 311 (firmware 101.01)
99 #  - reported/tested by Radek Kadner <radek.kadner@atspraha.cz>
100
101 sub _ie_ems_picture {
102         my ($self, %args) = @_;
103         my $bitmap = $args{"bitmap"};  # GSM::SMS::Bitmap instance
104         
105         $bitmap->crop(0xFF*8,0xFF);
106         my ($width, $height) = ($bitmap->{"width"}, $bitmap->{"height"});
107
108         my $iei;
109            if ($width == 16 && $height == 16) { $iei = IEI_PICTURE_SMALL   ; }
110         elsif ($width == 32 && $height == 32) { $iei = IEI_PICTURE_LARGE   ; }
111         else                                  { $iei = IEI_PICTURE_VARIABLE; }
112         $iei = IEI_PICTURE_VARIABLE if $args{"force_variable"};
113
114         return $self->_iebuilder($iei,
115                         $args{TEXT_POSITION}, # position in the SMS
116                         ($iei != IEI_PICTURE_VARIABLE ? () : (int(($width+7)/8), $height)),  # image size (optional)
117                         $bitmap->pixlist_horiz()
118         );
119 }
120
121 sub _ie_ems_animation {
122         my ($self, %args) = @_;
123         my $bitmaps = $args{"bitmaps"};  # list of GSM::SMS::Bitmap instances
124
125         if (4 != @$bitmaps) {
126                 carp "Invalid length of EMS animation (4 required)"; return ();
127         }
128         my $bitmap = $$bitmaps[0];
129         my ($width, $height) = ($bitmap->{"width"}, $bitmap->{"height"});
130         for my $testbitmap (@$bitmaps) {
131                 if ($width != $testbitmap->{"width"} || $height != $testbitmap->{"height"}) {
132                         carp "Non-matching sizes in EMS animation";
133                 }
134         }
135
136         my $iei;
137            if ($width ==  8 && $height ==  8) { $iei = IEI_ANIMATION_SMALL   ; }
138         elsif ($width == 16 && $height == 16) { $iei = IEI_ANIMATION_LARGE   ; }
139         else {
140                 $iei = IEI_ANIMATION_LARGE;
141                 carp "Invalid frame size of EMS animation (8x8 or 16x16 required)";
142         }
143
144         return $self->_iebuilder($iei,
145                         $args{TEXT_POSITION}, # position in the SMS
146                         &{sub {
147                                 my @r=();
148                                 for $bitmap (@$bitmaps) {
149                                         push @r,$bitmap->pixlist_horiz();
150                                 }
151                                 return @r;
152                         }}
153         );
154 }
155
156 sub _ie_alcatel {
157         my ($self, %args) = @_;
158
159         return $self->_iebuilder(IEI_ALCATEL,
160                         (!defined($args{"name"}) ? (0x00) :  # bit 7=0 (use GSM charset)
161                                         (length($args{"name"}), unpack("C*",
162                                                         encode_payload(0xF1, GSM::SMS::PDU->inversetranslate($args{"name"}))))  # 0xF1 DCS=any 7bit
163                                         ),
164                         (!!$args{"ems_compat"} << 7)  # bit 7=EMS compatibility
165                                         |($args{"alcatel_type"} & 0x07),  # bits 0..2=Alcatel message type
166                         ($args{"length"} >> 8), ($args{"length"} & 0xFF),
167         );
168 }
169
170 sub _udh_build {
171         my ($self, @srcs) = @_;
172
173         my @r=();
174         no strict 'refs';
175         my @list=map { &{"_ie_".$$_{"type"}}($self,%$_); } @srcs;
176         use strict 'refs';
177         return "" if !@list;
178         return pack "C*",scalar(@list),@list;
179 }
180
181 # Create a message from a payload
182 # returns either error message or list (DECONCAT_TOTAL,total messages)
183 sub _store_try {
184         my ($self, $number, $payload, %args) = @_;
185
186         # Reset the FRAME array
187         $self->{'__FRAMES__'} = [];
188
189
190         my $PDU = GSM::SMS::PDU->new();
191
192         my @udhs=($args{"udh"} ? @{$args{"udh"}} : ());
193
194         # We put ME-specific default if ANY element of UDH was specified
195         # Autodetect $dcs BEFORE possible concatenation UDH IEs get inserted!
196         my $dcs=$args{"dcs"} || (@udhs ? DEFAULT_DCS_UDH : DEFAULT_DCS_NOUDH);
197         # print "DCS: $dcs\n";
198
199         my $deconcat_ieref; # defined only if deconcatenating
200         if (defined $args{DECONCAT_TOTAL}) {
201                 my %deconcat_ie=(
202                                 "type"=>"concat8",
203                                 "drn"=>int(rand(0xFF)),        # reference number
204                                 "fmax"=>$args{DECONCAT_TOTAL}, # maximum number of short messages
205                                 "fsn"=>0x00,                   # sequence number of the current short message
206                 );
207                 $deconcat_ieref=\%deconcat_ie;
208                 push @udhs,$deconcat_ieref;
209                 return "Too long message (".$deconcat_ie{"fmax"}." elements)" if (0xFF < $deconcat_ie{"fmax"});
210         }
211
212         my @udhstatic=();
213         my @udhpositioned=();
214         for my $udh (@udhs) {
215                 if (exists $udh->{TEXT_POSITION}) {
216                         push(@udhpositioned, $udh);
217                 } else {
218                         push(@udhstatic, $udh);
219                 }
220         }
221         @udhpositioned = sort { $a->{TEXT_POSITION} <=> $b->{TEXT_POSITION}; } @udhpositioned;
222
223         my $position=0;           # absolute current position in the whole long SMS
224         while (length $payload || @udhpositioned) {
225                 my @udhpositioned_now=();    # items from @udhpositioned placed in the current SMS part
226                 my $payload_now="";          # text  from $payload       placed in the current SMS part
227                 while (1) {
228                         if (@udhpositioned && $udhpositioned[0]{TEXT_POSITION} <= $position) { # "<=" should be "=="
229                                 if (USERDATA_LENGTH < length($self->_udh_build(@udhstatic,@udhpositioned_now,$udhpositioned[0]))
230                                                 +nail_payload_len($dcs,length($payload_now))) {
231                                         return "UDH IE (User Data Header Information Element) too long to fit in one SMS fragment"
232                                                         if $payload_now eq "";
233                                         last;
234                                         }
235
236                                 my %udhei=%{shift @udhpositioned};  # copy it to local state - we will be modifying it
237                                 # we subtract the absolute position base of current SMS part:
238                                 $udhei{TEXT_POSITION} -= $position-length($payload_now);
239
240                                 push @udhpositioned_now,\%udhei;
241                                 next;
242                         }
243                         my $minlen=length $payload;
244                         $minlen=min($minlen,$udhpositioned[0]{TEXT_POSITION}-$position) if @udhpositioned;
245                         $minlen=min($minlen,empty_subload($dcs,
246                                         USERDATA_LENGTH-length($self->_udh_build(@udhstatic,@udhpositioned_now))-length($payload_now)))
247                                         if defined $args{DECONCAT_TOTAL};
248                         last if $minlen<=0; # "<=" should be "=="
249                         $payload_now.=substr($payload,0,$minlen);
250                         $payload=substr($payload,$minlen);
251                         $position+=$minlen;
252                 }
253
254                 # We will ship out the current SMS part here
255                 ++$deconcat_ieref->{"fsn"} if $deconcat_ieref;
256                 # We exceeded userdata size, this is probably the first undeconcatenaed try from store()!
257                 my $userdata=$self->_udh_build(@udhstatic,@udhpositioned_now).encode_payload($dcs,$payload_now);
258
259                 return (DECONCAT_TOTAL, undef) if USERDATA_LENGTH < length($userdata);
260
261                 my $pdu = $PDU->SMSSubmit(DEFAULT_SMS_CENTER, $number, $userdata,
262                                 $dcs, DEFAULT_VALIDITY, !!@udhstatic || !!@udhpositioned_now
263                                 );
264                 # Push on to frame array
265                 # print "--> $pdu\n"; 
266                 push(@{$self->{"__FRAMES__"}}, $pdu);
267         }
268
269         carp "Last fsn==".$deconcat_ieref->{"fsn"}." but fmax==".$deconcat_ieref->{"fmax"}
270                         if $args{DECONCAT_TOTAL} && $deconcat_ieref->{"fsn"}!=$deconcat_ieref->{"fmax"};
271         return (undef,(!$deconcat_ieref ? 1 : $deconcat_ieref->{"fsn"})); # success, return number of _PRODUCED_ MSes
272 }
273
274 sub store {
275         my ($self, $number, $payload, %args) = @_;
276
277         my ($err, $total);
278         ($err, $total) = $self->_store_try($number, $payload, %args, DECONCAT_TOTAL=>undef ); return $err if !$err || $err ne DECONCAT_TOTAL;
279         ($err, $total) = $self->_store_try($number, $payload, %args, DECONCAT_TOTAL=>0     ); return $err if $err;
280         ($err, $total) = $self->_store_try($number, $payload, %args, DECONCAT_TOTAL=>$total); return $err;
281 }
282
283 # Return the frames
284 sub get_frames {
285         my $self = shift;
286         return $self->{'__FRAMES__'};
287 }
288
289
290 1;
291
292 =head1 NAME
293
294 GSM::SMS::NBS::Message - SAR functionality for NBS messages.
295
296 =head1 DESCRIPTION
297
298 Implements the segmentation in the SAR engine ( Segmentation And Reassembly ).
299
300 =head1 AUTHOR
301
302 Johan Van den Brande <johan@vandenbrande.com>