File Coverage

blib/lib/Modem/VBox.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Modem::VBox;
2              
3 1     1   671 use strict 'subs';
  1         1  
  1         30  
4 1     1   5 use Carp;
  1         1  
  1         121  
5 1     1   936 use bytes;
  1         13  
  1         5  
6              
7             require Exporter;
8 1     1   847 use POSIX ':termios_h';
  1         7194  
  1         7  
9 1     1   1589 use Fcntl;
  1         2  
  1         323  
10 1     1   1533 use Event qw(unloop one_event time unloop_all);
  0            
  0            
11             use Event::Watcher qw(R W);
12             use Time::HiRes qw/time/; # this is required(!)
13              
14             BEGIN { $^W=0 } # I'm fed up with bogus and unnecessary warnings nobody can turn off.
15              
16             @ISA = qw(Exporter);
17              
18             @_consts = qw(RING RUNG CONNECT BREAK EOTX);
19             @_funcs = qw();
20              
21             @EXPORT = @_consts;
22             @EXPORT_OK = @_funcs;
23             %EXPORT_TAGS = (all => [@_consts,@_funcs], constants => \@_consts);
24             $VERSION = '0.051';
25              
26             # if debug is used, STDIN will be used for events and $play will be used to play messages
27             $debug = 0;
28              
29             # hardcoded constants
30             $HZ=8000;
31             $PFRAG=8192; # frag size for play_pause
32              
33             $ETX="\003";
34             $DLE="\020";
35             $DC4="\024";
36              
37             # bit flags for state var
38             sub VCON (){ 1 }
39             sub VTX (){ 2 }
40             sub VRX (){ 4 }
41              
42             # event types
43             sub RING (){ -1 } # a single ring (+ count)
44             sub RUNG (){ -2 } # ring timeout
45             sub CONNECT (){ -3 } # a single ring (+ count)
46             sub BREAK (){ -4 } # break sequence detected
47             sub EOTX (){ -6 } # end of current transmissions
48              
49             sub slog {
50             my $self=shift;
51             my $level=shift;
52             print STDERR $self->{line},": ",@_,"\n" if $level <= $debug;
53             }
54              
55             # port => /dev/ttyI0
56             sub new {
57             my $class = shift;
58             my(%attr)=@_;
59              
60             croak "line must be specified" unless $attr{line};
61              
62             eval { $attr{speed} ||= &B115200 };
63             eval { $attr{speed} ||= &B57600 };
64             $attr{speed} ||= B38400;
65             $attr{timeout} ||= 2;
66              
67             $attr{dropdtrtime} ||= 0.25; # dtr timeout
68             $attr{modeminit} ||= "ATZ";
69             $attr{ringto} ||= 6; # ring-timeout
70             $attr{rings} ||= 3; # number of rings
71              
72             $attr{ring_cb} ||= sub { };
73              
74             my $self = bless \%attr,$class;
75              
76             $self->{ringtowatcher} = Event->timer(
77             interval => $self->{ringto},
78             desc => "RING timeout watcher",
79             parked => 1,
80             cb => sub {
81             $self->rung;
82             $self->slog(1, "ring timeout, aborted connection");
83             }
84             );
85              
86             $self->initialize;
87             $self->reset;
88              
89             $self->{HZ} ||= $HZ;
90             $self->{FRAG} ||= 1024;
91              
92             $self;
93             }
94              
95             sub DESTROY {
96             my $self=shift;
97             $self->{tio}->setispeed(B0); $self->{tio}->setospeed(B0); $self->sane;
98             close $self->{fh} or croak "error during modem-close: $!";
99             }
100              
101             sub flush {
102             my $self=shift;
103             undef $self->{rawinput};
104             tcflush $self->{fileno}, TCIOFLUSH;
105             my $buf; 1 while (sysread ($self->{fh},$buf,1024) > 0);
106             }
107              
108             sub sane {
109             my $self=shift;
110             $self->{tio}->setiflag(BRKINT|IGNPAR|IXON);
111             $self->{tio}->setoflag(OPOST);
112             $self->{tio}->setcflag($self->{tio}->getcflag
113             &~(CSIZE|CSTOPB|PARENB|PARODD|CLOCAL)
114             | (CS8|CREAD|HUPCL));
115             $self->{tio}->setlflag(ECHOK|ECHOE|ECHO|ISIG|ICANON);
116             $self->{tio}->setattr($self->{fileno});
117             $self->{tio}->setcc(VMIN,1);
118             $self->{tio}->setcc(VTIME,0);
119             }
120              
121             sub raw {
122             my $self=shift;
123             $self->{tio}->setiflag($self->{tio}->getiflag & (IXON|IXOFF));
124             $self->{tio}->setoflag(0);
125             $self->{tio}->setcflag(0);
126             $self->{tio}->setlflag(0);
127             $self->{tio}->setcc(VMIN,1);
128             $self->{tio}->setcc(VTIME,0);
129             $self->{tio}->setattr($self->{fileno});
130             }
131              
132             sub reset {
133             my $self=shift;
134              
135             $self->initialize;
136             $self->sane;
137             $self->{inwatcher}->stop;
138              
139             my $i=$self->{tio}->getispeed; my $o=$self->{tio}->getospeed;
140             $self->{tio}->setispeed(B0); $self->{tio}->setospeed(B0);
141              
142             $self->{tio}->setattr($self->{fileno});
143             my $w = Event->timer(after => $self->{dropdtrtime},
144             cb => sub { $_[0]->w->cancel; unloop },
145             desc => 'Modem DTR drop timeout');
146              
147             $self->{tio}->setispeed($i); $self->{tio}->setospeed($o);
148              
149             $self->slog(3,"waiting for reset");
150             $self->loop;
151             $self->slog(3,"line reset");
152              
153             $self->{tio}->setattr($self->{fileno});
154              
155             $self->raw;
156             $self->flush;
157             $self->{inwatcher}->start;
158              
159             $self->command("AT")=~/^OK/ or croak "modem returned $self->{resp} to AT";
160             $self->command($self->{modeminit})=~/^OK/ or croak "modem returned $self->{resp} to modem init string";
161             $self->command("AT+VLS=2")=~/^OK/ or croak "modem returned $self->{resp} to AT+VLS=2";
162             $self->command("AT+VSM=6")=~/^OK/ or croak "modem returned $self->{resp} to AT+VSM=6";
163             }
164              
165             # read a line
166             sub modemline {
167             my $self=shift;
168             my $timeout;
169             Event->timer (
170             after => $self->{timeout},
171             desc => "modem response timeout",
172             cb => sub { $timeout = 1;
173             $_[0]->w->cancel }
174             );
175             one_event while !@{$self->{modemresponse}} && !$timeout;
176             shift(@{$self->{modemresponse}});
177             }
178              
179             sub modemwrite {
180             my $self = shift;
181             my $cmd = shift;
182             fcntl $self->{fh},F_SETFL,0;
183             syswrite $self->{fh}, $cmd, length $cmd;
184             fcntl $self->{fh},F_SETFL,O_NONBLOCK;
185             }
186              
187             sub command {
188             my $self = shift;
189             my $cmd = shift;
190             $self->modemwrite("$cmd\r");
191             $self->{resp} = $self->modemline;
192             $self->{resp} = $self->modemline if $self->{resp} eq $cmd;
193             $self->slog(2,"COMMAND($cmd) => ",$self->{resp});
194             $self->{resp};
195             }
196              
197             sub initialize {
198             my $self=shift;
199              
200             $self->{inwatcher}->cancel if $self->{inwatcher};
201             $self->{outwatcher}->cancel if $self->{outwatcher};
202              
203             delete @{$self}{qw(play_queue state context break callerid
204             rawinput rawoutput modemresponse record
205             inwatcher outwatcher tio fh)};
206              
207             $self->slog(3,"opening line");
208              
209             $self->{fh}=local *FH;
210             sysopen $self->{fh},$self->{line},O_RDWR|O_NONBLOCK
211             or croak "unable to open device $self->{line} for r/w";
212             $self->{fileno}=fileno $self->{fh};
213              
214             $self->{tio} = new POSIX::Termios;
215             $self->{tio}->getattr($self->{fileno});
216              
217             $self->{inwatcher}=Event->io(
218             poll => R,
219             fd => $self->{fileno},
220             desc => "Modem input for $self->{line}",
221             parked => 1,
222             cb => sub {
223             my $ri = \($self->{rawinput});
224             if (sysread($self->{fh}, $$ri, 8192, length $$ri) == 0) {
225             $self->slog(1, "short read, probably remote hangup");
226             if ($self->connected) {
227             #$self->{state} &= ~(VCON|VRX|VTX);
228             $self->hangup;
229             } else {
230             $self->slog(0, "WOAW, short read while in command mode, reinitialize");
231             $self->initialize;
232             }
233             } else {
234             if ($self->{state} & VRX) {
235             my $changed;
236             # must use a two-step process
237             $$ri =~ s/^((?:[^$DLE]+|$DLE[^$ETX$DC4])*)//o;
238             my $data = $1;
239             $data =~ s{$DLE(.)}{
240             if ($1 eq $DLE) {
241             $DLE;
242             } else {
243             $self->{break} .= $1;
244             $changed=1;
245             "";
246             }
247             }ego;
248             $self->{record}->($data) if $self->{record};
249             if ($$ri =~ s/^$DLE$ETX//o) {
250             $self->slog(3, "=> ETX, EO VTX|VRX");
251             $self->{state} &= ~VRX;
252             if ($self->{state} & VTX) {
253             $self->{state} &= ~VTX;
254             delete $self->{play_queue};
255             delete $self->{rawoutput};
256             $self->modemwrite("$DLE$ETX");
257             }
258             $$ri =~ s/^[\r\n]*(?:VCON)?[\r\n]+//;
259             }
260             $self->check_break if $changed;
261             }
262             unless ($self->{state} & VRX) {
263             while ($$ri =~ s/^([^\r\n]*)[\r\n]+//) {
264             local $_ = $1;
265             if (/^CALLER NUMBER:\s+(\d+)$/) {
266             $self->{_callerid}=$1;
267             $self->slog(3,"incoming call has callerid $1");
268             } elsif (/^RING\b/) {
269             my $cid = delete $self->{_callerid} || "0";
270             my $oci = $self->{callerid};
271             $self->{callerid}=$cid;
272             if (defined $oci) {
273             if ($oci ne $cid) {
274             $self->rung;
275             }
276             } else {
277             $self->{ring}=0;
278             }
279             $self->{ringtowatcher}->stop;
280             $self->{ringtowatcher}->again;
281             $self->{ring}++;
282             $self->{ring_cb}->($self->{ring}, $self->{callerid});
283             $self->slog(1, "the telephone rings (#".($self->{ring})."), hurry! (callerid $self->{callerid})");
284             $self->accept if $self->{ring} >= $self->{rings};
285             } elsif (/^RUNG\b/) {
286             $self->rung;
287             } elsif (/\S/) {
288             push @{$self->{modemresponse}}, $_;
289             }
290             }
291             }
292             }
293             }
294             );
295             $self->{outwatcher} = Event->timer(
296             parked => 1,
297             desc => "Modem sound output for $self->{line}",
298             cb => sub {
299             my $w = $_[0]->w;
300             my $l;
301             unless (length $self->{rawoutput}) {
302             my $q = $self->{play_queue};
303             if (@$q) {
304             #$self->slog(7, "(out $q->[0])");
305             if (ref \($q->[0]) eq "GLOB") {
306             my $n;
307             $l = sysread $q->[0], $self->{rawoutput}, $self->{FRAG};
308             #$self->slog(7, "reading from file ($l bytes)\n");#d#
309             $self->{rawoutput} =~ s/$DLE/$DLE$DLE/go;
310             if ($l <= 0) {
311             #$self->slog(7, "EOTX\n");#d#
312             $self->event(EOTX, scalar@$q);
313             shift @$q;
314             }
315             } else {
316             $self->{rawoutput} = ${shift(@$q)};
317             }
318             } else {
319             $w->stop;
320             $self->event(EOTX, 0);
321             return;
322             }
323             }
324             if (length $self->{rawoutput}) {
325             #$self->slog(7, "(send ".(length $self->{rawoutput})." bytes)");
326             $l = syswrite $self->{fh}, $self->{rawoutput}, length $self->{rawoutput};
327             #$self->slog(7, "(sent $l bytes)");
328             substr($self->{rawoutput}, 0, $l) = "" if $l > 0;
329             if (defined $l) {
330             $l /= $self->{HZ}; #/
331             } else {
332             $l = 0.1;
333             }
334             $self->{vtx_end} += $l;
335             }
336             $w->at($self->{vtx_end} - 0.01);
337             $w->start;
338             }
339             );
340              
341             $self->{tio}->setispeed($self->{speed});
342             $self->{tio}->setospeed($self->{speed});
343              
344             $self->{ring}=0;
345             }
346              
347             sub abort {
348             my $self=shift;
349             $self->initialize;
350             $self->reset;
351             $self->slog(1,"modem is now in listening state");
352             }
353              
354             sub rung {
355             my $self=shift;
356             $self->{ringtowatcher}->stop;
357             $self->{ring}=0;
358             $self->event(RUNG);
359             $self->slog(1,"caller ($self->{callerid}) hung up before answering");
360             delete $self->{callerid};
361             }
362              
363             sub loop {
364             local $Event::DIED = sub {
365             print STDERR $_[1];
366             unloop_all;
367             };
368             Event::loop;
369             }
370              
371             sub accept {
372             my $self=shift;
373             # DLE etc. handling
374             $self->{ringtowatcher}->stop;
375             if ($self->command("ATA") =~ /^VCON/) {
376             $self->slog(2, "call accepted (callerid $self->{callerid})");
377             if ($self->command("AT+VTX+VRX") =~ /^CONNECT/) {
378             $self->{state} |= VCON|VTX|VRX;
379             delete $self->{event};
380             $self->event(CONNECT);
381             $self->{connect_cb}->($self);
382             delete $self->{event};
383             } else {
384             $self->rung;
385             $self->abort;
386             $self->slog(1, "modem did not respond with CONNECT to AT+VTX+VRX command");
387             }
388             } else {
389             $self->slog(1, "modem did not respond with VCON to my ATA");
390             $self->rung;
391             }
392             }
393              
394             sub check_break {
395             my $self=shift;
396             while(my($k,$v) = each %{$self->{context}}) {
397             if ($self->{break} =~ /$k/) {
398             ref $v eq "CODE" ? $v->($self, $self->{break})
399             : $self->event(BREAK, $v);
400             }
401             }
402             }
403              
404             sub hangup {;
405             my $self=shift;
406             $self->event(undef) if $self->connected;
407             $self->abort;
408             }
409              
410             sub connected {
411             $_[0]->{state} & VCON;
412             }
413              
414             # return the number of pending events
415             sub pending {
416             @{$_[0]->{event}};
417             }
418              
419             sub wait_event {
420             my $self = shift;
421             one_event while !$self->pending;
422             }
423              
424             sub event {
425             my $self=shift;
426             #$self->slog(3, "EVENT ".(scalar@_)." :@_:");
427             if (@_) {
428             push @{$self->{event}},
429             defined $_[0] ? bless [@_], "Modem::VBox::Event"
430             : undef;
431             } else {
432             $self->wait_event;
433             defined $self->{event}->[0] ? shift @{$self->{event}}
434             : undef;
435             }
436             }
437              
438             sub play_file($$) {
439             my $self = shift;
440             my $path = shift;
441             my $fh = do { local *FH };
442             $self->slog(5, "play_file $path");
443             open $fh,"<$path" or croak "unable to open ulaw file '$path' for playing";
444             $self->play_object($fh);
445             }
446              
447             sub play_data($$) {
448             my $self=shift;
449             my $data=shift;
450             $data=~s/$DLE/$DLE$DLE/go;
451             $self->play_object(\$data);
452             }
453              
454             sub play_object($$) {
455             my $self=shift;
456             my $obj=shift;
457             $self->{state} & VCON or return;
458             unless ($self->{outwatcher}->is_active) {
459             $self->{outwatcher}->at($self->{vtx_end} = time);
460             $self->{outwatcher}->start;
461             }
462             push @{$self->{play_queue}}, $obj;
463             }
464              
465             sub play_pause($$) {
466             my $self=shift;
467             $self->slog(5, "play_pause $_[0]");
468             my $len = int($self->{HZ}*$_[0]+0.999);
469             my $k8 = "\xFE" x $PFRAG;
470             while ($len>length($k8)) {
471             $self->play_object(\$k8);
472             $len-=length($k8);
473             }
474             $self->play_object(\("\xFE" x $len));
475             }
476              
477             sub play_count($) {
478             scalar @{$_[0]->{play_queue}};
479             }
480              
481             sub play_flush($) {
482             my $self=shift;
483             #tcflush $self->{fileno}, TCOFLUSH;
484             @{$self->{play_queue}} = ();
485             delete $self->{rawoutput};
486             one_event;
487             }
488              
489             sub play_drain($) {
490             my $self=shift;
491             my $waiting = 1;
492             one_event while $self->play_count;
493             Event->timer(at => $self->{vtx_end},
494             desc => "play_drain timer",
495             cb => sub { $waiting = 0;
496             $_[0]->w->cancel }
497             );
498             one_event while $waiting;
499             }
500              
501             sub record($$) {
502             my $self = shift;
503             $self->{record} = shift;
504             }
505              
506             sub record_file($$) {
507             my $self = shift;
508             my $fh = shift;
509             $self->record (sub { print $fh $_[0] });
510             }
511              
512             sub callerid($) { $_[0]->{callerid} }
513              
514             sub context($) {
515             my $self=shift;
516             bless [$self, {%{$self->{context}}}], "Modem::VBox::context";
517             }
518              
519             package Modem::VBox::Event;
520              
521             sub type($$) { $_[0]->[0] == $_[1] }
522             sub isbreak($) { $_[0]->[0] == Modem::VBox::BREAK }
523             sub iseotx($;$) { $_[0]->[0] == Modem::VBox::EOTX && ( @_ < 2 || $_[1] >= $_[0]->[1] ) }
524             sub data($) { $_[0]->[1] }
525              
526             package Modem::VBox::context;
527              
528             sub set {
529             my $self=shift;
530             %{$self->[0]{context}} = @_;
531             $self;
532             }
533              
534             *clr = \&set;
535              
536             sub add {
537             my $self=shift;
538             while(@_) {
539             $self->[0]{context}{$_[0]} = $_[1];
540             shift; shift;
541             }
542             $self;
543             }
544              
545             sub del {
546             my $self=shift;
547             for(@_) {
548             delete $self->[0]{context}{$_};
549             }
550             $self;
551             }
552              
553             sub DESTROY {
554             my $self=shift;
555             my($vbox,$ctx)=@$self;
556             $vbox->{context}=$ctx;
557             }
558              
559             1;
560             __END__