File Coverage

blib/lib/Device/CableModem/SURFboard.pm
Criterion Covered Total %
statement 58 172 33.7
branch 8 46 17.3
condition 3 16 18.7
subroutine 13 30 43.3
pod 17 18 94.4
total 99 282 35.1


line stmt bran cond sub pod time code
1             package Device::CableModem::SURFboard;
2 1     1   20582 use strict;
  1         5  
  1         41  
3             #use warnings; # testing
4              
5             BEGIN {
6 1     1   6 use Exporter ();
  1         2  
  1         21  
7 1     1   5 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         7  
  1         116  
8 1     1   2 $VERSION = '0.04';
9 1         12 @ISA = qw(Exporter);
10 1         9 @EXPORT = qw();
11 1         2 @EXPORT_OK = qw(&errstr);
12 1         24 %EXPORT_TAGS = ();
13             }
14              
15             # Device::CableModem::SURFboard - Motorola 'SURFboard' modem status
16             # (models: SB4100, SB4200, SB5100, SB5100E, SB5101, SBV5120E)
17              
18             # See the bottom of this file for the POD documentation. Search for
19             # the string '=head'.
20              
21             # You can run this file through either pod2man or pod2html to produce
22             # pretty documentation in manual or html file format (these utilities
23             # are part of the Perl 5 distribution).
24              
25             # copyright(C) 2007 Scott Mazur
26              
27             # requires:
28             # Socket;
29             # Scalar::Util;
30              
31 1     1   881 use Socket;
  1         3957  
  1         666  
32 1     1   7 use Scalar::Util;
  1         2  
  1         56  
33              
34 1     1   7 use constant SB5100_PATH => '/signaldata.html';
  1         1  
  1         74  
35 1     1   5 use constant SB5101_PATH => '/RgSignal.asp';
  1         2  
  1         34  
36 1     1   4 use constant SBV5120E_PATH => '/cmSignalData.htm';
  1         1  
  1         1624  
37              
38             my $errstr = '';
39             my $errfatal = 0;
40              
41             sub new
42             {
43 1     1 0 16 my ($class, %parameters) = @_;
44              
45 1         13 my $self = {
46             dnPowerMax => 16,
47             dnPowerMin => -16,
48             upPowerMax => 54,
49             upPowerMin => 36,
50             SNRatioMax => 100, # FIXME reasonable value?
51             SNRatioMin => 0, # FIXME reasonable value?
52             modemIP => '192.168.100.1',
53             loginUsername => 'admin',
54             loginPassword => 'motorola',
55             %parameters,
56             };
57              
58             # to prevent trying more page paths if the ip can't connect
59 1         2 $errfatal = 0;
60              
61             # get the modem status page
62 1   50     5 my $page_ref = pageRef($self, SB5100_PATH, 'SB5100')
63             || pageRef($self, SB5101_PATH, 'SB5101')
64             || pageRef($self, SBV5120E_PATH, 'SBV5120E')
65             || return undef;
66              
67             # The SBV5120E uses a login page.
68             # Because of this the first page request sent to the modem
69             # is completely ignored and the first response returned
70             # is the login page. A second page request (or more) is
71             # required to complete the login and get the signal page,
72             # after which the modem will remain 'logged in' for some period
73             # of time.
74 0 0 0     0 if ($self->{modelGroup} eq 'SBV5120E'
75             and $$page_ref =~ m/loginUsername/i) {
76 0         0 my $path = '/loginData.htm' .
77             '?loginUsername=' . $self->{loginUsername} .
78             '&loginPassword=' . $self->{loginPassword} .
79             '&LOGIN_BUTTON=Login';
80 0         0 my $tries = 4;
81 0   0     0 while (--$tries and $$page_ref =~ m/loginUsername/i) {
82 0 0       0 $page_ref = pageRef($self, $path)
83             or return undef;
84             }
85 0 0       0 if (!$tries) {
86 0         0 $errstr = "Failed to pass login page!";
87 0         0 return undef;
88             }
89             # now get the try the signal page again
90 0         0 $page_ref = pageRef($self, SBV5120E_PATH)
91             }
92              
93             # clean up the html a bit for parsing
94 0         0 $$page_ref =~ s/\n//g; # drop new lines
95 0         0 $$page_ref =~ s!! !ig; # strip table tags
96 0         0 $$page_ref =~ s/ / /ig; # replace hard spaces
97 0         0 $$page_ref =~ s/\s\s+/ /g; # reduce double spaces
98              
99             # check that the page has what we expect
100 0 0       0 if ($$page_ref =~ m{
101             Frequency \s (\d+)\s (Hz) \s (Locked\s)?
102             Signal \s To \s Noise \s Ratio \s ([\d.]+)\s (dB) \s
103             .*? # non-greedy extra stuff for SBV5120E
104             Power \s Level \s ([\d.-]+)\s (dBmV)
105             }xi) {
106             # fill in the signal strength values
107 0         0 $self->{dnFreq} = $1;
108 0         0 $self->{dnFreqUnit} = $2;
109 0         0 $self->{SNRatio} = $4;
110 0         0 $self->{SNRatioUnit} = $5;
111 0         0 $self->{dnPower} = $6;
112 0         0 $self->{dnPowerUnit} = $7;
113             }
114             else {
115 0         0 $errstr = "Failed to parse content!";
116 0         0 return undef;
117             }
118              
119             # get the upstream values
120 0 0       0 if ($$page_ref =~ m{
121             ID \s (\d+) \s
122             Frequency \s (\d+)\s (Hz)(\s Ranged)? \s
123             .*? # non-greedy extra stuff for SBV5120E
124             Power(\s Level)?\s ([\d.-]+)\s (dBmV)
125             }xi) {
126 0         0 $self->{channel} = $1;
127 0         0 $self->{upFreq} = $2;
128 0         0 $self->{upFreqUnit} = $3;
129 0         0 $self->{upPower} = $6;
130 0         0 $self->{upPowerUnit} = $7;
131             }
132             else {
133             # default
134 0         0 $self->{channel} = 0;
135 0         0 $self->{upFreq} = 0;
136 0         0 $self->{upFreqUnit} = 'Hz';
137 0         0 $self->{upPower} = 0;
138 0         0 $self->{upPowerUnit} = 'dBmV';
139             }
140              
141             # $errstr should be blank by now
142 0         0 $self->{errstr} = $errstr;
143 0   0     0 return bless($self, ref ($class) || $class);
144             }
145              
146             sub errstr {
147 1     1 1 12 my $self = shift;
148 1 50       7 return $self->{errstr} if Scalar::Util::blessed $self;
149 1         5 return $errstr;
150             }
151              
152 0     0 1 0 sub modelGroup { shift->{modelGroup} }
153              
154 0     0 1 0 sub channel { shift->{channel} }
155              
156             # up freq value + unit string
157             sub upFreqStr {
158 0     0 1 0 my $self = shift;
159 0         0 "$self->{upFreq} $self->{upFreqUnit}";
160             }
161 0     0 1 0 sub upFreq { shift->{upFreq} }
162              
163             # down freq value + unit string
164             sub dnFreqStr {
165 0     0 1 0 my $self = shift;
166 0         0 "$self->{dnFreq} $self->{dnFreqUnit}";
167             }
168 0     0 1 0 sub dnFreq { shift->{dnFreq} }
169              
170             # SNRatio value + unit string
171             sub SNRatioStr {
172 0     0 1 0 my $self = shift;
173 0         0 "$self->{SNRatio} $self->{SNRatioUnit}";
174             }
175 0     0 1 0 sub SNRatio { shift->{SNRatio} }
176              
177             # compare SNRatio value with limits
178 0     0 1 0 sub SNRatioCheck { my $self = shift;
179 0         0 my $level = $self->{SNRatio};
180 0 0       0 return 'high' if $level > $self->{SNRatioMax};
181 0 0       0 return 'low' if $level < $self->{SNRatioMin};
182 0         0 return '';
183             }
184              
185             # down power value + unit string
186             sub dnPowerStr {
187 0     0 1 0 my $self = shift;
188 0         0 "$self->{dnPower} $self->{dnPowerUnit}";
189             }
190 0     0 1 0 sub dnPower { shift->{dnPower} }
191              
192             # compare downstream Power value with limits
193 0     0 1 0 sub dnPowerCheck { my $self = shift;
194 0         0 my $level = $self->{dnPower};
195 0 0       0 return 'high' if $level > $self->{dnPowerMax};
196 0 0       0 return 'low' if $level < $self->{dnPowerMin};
197 0         0 return '';
198             }
199              
200             # up power value + unit string
201             sub upPowerStr {
202 0     0 1 0 my $self = shift;
203 0         0 "$self->{upPower} $self->{upPowerUnit}";
204             }
205 0     0 1 0 sub upPower { shift->{upPower} }
206              
207             # compare upstream Power value with limits
208 0     0 1 0 sub upPowerCheck { my $self = shift;
209 0         0 my $level = $self->{upPower};
210 0 0       0 return 'high' if $level > $self->{upPowerMax};
211 0 0       0 return 'low' if $level < $self->{upPowerMin};
212 0         0 return '';
213             }
214              
215             # connect to the modem and retrieve the page in $path
216 3     3 1 8 sub pageRef { my $self = shift;
217             # if the ip connect failed once, there's no point trying again
218 3 100       51 return undef if $errfatal;
219              
220 1         3 my $path = shift;
221 1 50       4 if (!$path) {
222 0         0 $errstr = 'No page path';
223 0         0 return undef;
224             }
225             # remember model group (future enhancement)
226 1   50     6 my $model_group = shift || $self->{modelGroup} || '';
227              
228 1   33     16 my $modem_ip = shift || $self->{modemIP};
229 1 50       5 if (!$modem_ip) {
230 0         0 $errstr = 'No modem IP';
231 0         0 return undef;
232             }
233              
234 1         2 my $buf;
235              
236             # open a tcp socket to the modem
237 1         1143 socket(MODEM, PF_INET, SOCK_STREAM, getprotobyname('tcp'));
238              
239             # connect with timeout
240 1         4 my $timeout_failed = 1;
241 1         3 eval {
242             # set a signal to die if the timeout is reached
243 1     1   39 local $SIG{ALRM} = sub { die "alarm\n" };
  1         1000159  
244             # modem response should be quick!
245 1         15 alarm 1; # 1 second
246 1 50       23 connect(MODEM, sockaddr_in(80, inet_aton($modem_ip)))
247             or $errfatal++;
248 0         0 alarm 0;
249 0 0       0 $errstr = "Couldn't connect to $modem_ip:80 : $!"
250             if $errfatal;
251 0         0 $timeout_failed = 0;
252             };
253 1         10 alarm 0; # prevent race condition
254              
255             # error in connect
256 1 50       12 return undef if $errfatal;
257              
258             # connect timeout
259 1 50       5 if ($timeout_failed) {
260 1         64 close(MODEM);
261 1         2 $errfatal++;
262 1         7 $errstr = "Couldn't connect to $modem_ip:80 : Socket Timeout";
263 1         20 return undef;
264             }
265              
266              
267             # enable command buffering (autoflush)
268 0           select((select(MODEM), $| = 1)[0]);
269              
270             # send the page request with timeout
271 0           $timeout_failed = 1;
272 0           eval {
273 0     0     local $SIG{ALRM} = sub { die "alarm\n" };
  0            
274 0           alarm 1; # 1 second
275 0           print MODEM join("\015\012",
276             "GET $path HTTP/1.0",
277             # "GET $path HTTP/1.1",
278             "Host: $modem_ip",
279             "User-Agent: ". __PACKAGE__ ."/$VERSION",
280             # "User-Agent: Cable-Modem/$VERSION",
281             # "From: root\@localhost",
282             "", "");
283 0           alarm 0;
284 0           $timeout_failed = 0;
285             };
286 0           alarm 0; # prevent race condition
287 0 0         if ($timeout_failed) {
288 0           close(MODEM);
289 0           $errstr = "Couldn't send to $modem_ip:80 : Socket Timeout";
290 0           return undef;
291             }
292              
293             # get the page results with timeout
294 0           $timeout_failed = 1;
295 0           eval {
296 0           local $/; # slurp the page
297 0           undef $/;
298             # set a signal to die if the timeout is reached
299 0     0     local $SIG{ALRM} = sub { die "alarm\n" };
  0            
300 0           alarm 1; # 1 second
301 0           $buf = ;
302 0           alarm 0;
303 0           $timeout_failed = 0;
304             };
305 0           alarm 0; # prevent race condition
306 0 0         if ($timeout_failed) {
307 0           close(MODEM);
308 0           $errstr = "Couldn't get from $modem_ip:80 : Socket Timeout";
309 0           return undef;
310             }
311              
312 0 0         if ($buf =~ m,^HTTP/\d+\.\d+\s+(\d+)[^\012]*\012,) {
313 0           my $code = $1;
314              
315             # we don't handle redirects
316 0 0         if ($code !~ /^2/) {
317 0           $errstr = "Bad page code $code";
318 0           return undef;
319             }
320              
321 0           $errstr = ''; # clear the error message
322 0           $buf =~ s/.+?\015?\012\015?\012//s; # zap header
323 0           $self->{modelGroup} = $model_group;
324 0           return \$buf;
325             }
326              
327 0           $errstr = "Unknown page response";
328 0           return undef;
329             }
330              
331              
332             #################### main pod documentation begin ###################
333              
334             =head1 NAME
335              
336             Device::CableModem::SURFboard - Get info from a Motorola 'SURFboard'
337              
338             =head1 SYNOPSYS
339              
340             use Device::CableModem::SURFboard;
341             my $modem = Device::CableModem::SURFboard->new
342             or die Device::CableModem::SURFboard->errstr;
343              
344             # print upstream power range check
345             print $modem->upPowerStr . ' ' . $modem->upPowerCheck;
346              
347             # print downstream power range check
348             print $modem->dnPowerStr . ' ' . $modem->dnPowerCheck;
349              
350             # print Signal/Noise range check
351             print $modem->SNRatioStr . ' ' . $modem->SNRatioCheck;
352              
353             =head1 DESCRIPTION
354              
355             The Motorola 'SURFboard' cable modem includes a built in web interface
356             that contains useful information like signal to noise ratios and power
357             levels. These values can be used to aid in trouble shooting modem
358             connection problems, or monitoring the health of the modem or cable
359             connection.
360              
361             C connects to several different models
362             of 'SURFboard' modems (currently confirmed: SB4100, SB4200, SB5100,
363             SB5100E, SB5101, SBV5120E), scraping the status page for the most
364             useful information regarding cable line condition.
365              
366             =head2 CREATING A NEW MODEM OBJECT
367              
368             $modem = Device::CableModem::SURFboard->new();
369              
370             This will create a new modem object using default values. You can
371             also initialize the modem object from an associative array reference:
372              
373             $modem = Device::CableModem::SURFboard->new(
374             dnPowerMax => 16,
375             dnPowerMin => -16,
376             upPowerMax => 54,
377             upPowerMin => 36,
378             SNRatioMax => 100,
379             SNRatioMin => 0,
380             modemIP => '192.168.100.1',
381             loginUsername => 'admin',
382             loginPassword => 'motorola');
383              
384             The above example also demonstrates all of the configurable options
385             with their defaults.
386              
387             =head1 METHODS
388              
389             =over 2
390              
391             =item errstr()
392              
393             Returns the last error message (or empty). Currently this isn't much
394             use as a method as only pageRef() (used internally) will generate
395             errors. errstr() can also be called directly to determine why a
396             new() method failed.
397              
398             =item modelGroup()
399              
400             Returns the model group found. Different models of SURFboard modems
401             have different URL/page layouts. These can be grouped into similar
402             model groups that share the same basic layout. When a new modem
403             object is created, modelGroup will be set according to the first
404             successful status page retrieved.
405              
406             =item channel()
407              
408             Returns the up stream channel id (number).
409              
410             =item upFreq()
411              
412             Returns the up stream frequency value (Hz) as a simple number.
413              
414             =item upFreqStr()
415              
416             Returns the up stream frequency value as a text string with the unit
417             description attached. For example "25250000 Hz".
418              
419             =item dnFreq()
420              
421             Returns the down stream frequency value (Hz) as a simple number.
422              
423             =item dnFreqStr()
424              
425             Returns the down stream frequency value as a text string with the
426             unit description attached. For example "477000000 Hz".
427              
428             =item SNRatio()
429              
430             Returns the down stream Signal to Noise ratio value (dB) as a simple
431             number.
432              
433             =item SNRatioStr()
434              
435             Returns the down stream Signal to Noise ratio value as a text string
436             with the unit description attached. For example "40.5 dB".
437              
438             =item SNRatioCheck()
439              
440             Checks the current down stream Signal to Noise ratio against pre-
441             defined max/min limits and returns either "high", "low" or blank.
442             The pre-defined max/min (default 100/0) can be also be set with
443             the SNRatioMax/SNRatioMin parameters when the object is created.
444              
445             =item dnPower()
446              
447             Returns the down stream power value (dBmV) as a simple number.
448              
449             =item dnPowerStr()
450              
451             Returns the down stream power value as a text string with the
452             unit description attached. For example "7.3 dBmV".
453              
454             =item dnPowerCheck()
455              
456             Checks the current down stream power against pre-defined max/min
457             limits and returns either "high", "low" or blank. The pre-defined
458             max/min (default 16/-16) can be also be set with the
459             dnPowerMax/dnPowerMin parameters when the object is created.
460              
461             =item upPower()
462              
463             Returns the up stream power value (dBmV) as a simple number.
464              
465             =item upPowerStr()
466              
467             Returns the up stream power value as a text string with the
468             unit description attached. For example "49.5 dBmV".
469              
470             =item upPowerCheck()
471              
472             Checks the current up stream power against pre-defined max/min
473             limits and returns either "high", "low" or blank. The pre-defined
474             max/min (default 54/36) can be also be set with the
475             upPowerMax/upPowerMin parameters when the object is created.
476              
477             =item pageRef()
478              
479             Takes a URL path, optional modem group id and optional IP address to
480             read a page from the modem. pageRef() is used internally to get the
481             signal information page. It could also be used for grabbing other
482             information pages from modems. A valid page request returns a reference
483             to a string containing the page contents. A page request failure will
484             return undefined (call errstr() to find out why).
485              
486             =back
487              
488             =head1 SUPPORT
489              
490             This script was developed and tested on a Motorla SURFboard cable modem.
491             (Models: SB4100, SB4200, SB5100, SB5100E, SB5101, SBV5120E).
492              
493             It may work on other Motorla modems, but likely will not. If you have
494             a different cable modem that works, or you would like to have work,
495             please let me know.
496              
497             =head1 AUTHOR
498              
499             Scott Mazur
500             CPAN ID: RUZAM
501             littlefish.ca
502             scott@littlefish.ca
503             http://littlefish.ca
504              
505             =head1 COPYRIGHT
506              
507             copyright(C) 2007 Scott Mazur, all rights reserved.
508              
509             This program is free software; you can redistribute
510             it and/or modify it under the same terms as Perl itself.
511              
512             The full text of the license can be found in the
513             LICENSE file included with this module.
514              
515              
516             =head1 SEE ALSO
517              
518             perl(1).
519              
520             =cut
521              
522             #################### main pod documentation end ###################
523              
524              
525             1;
526             # The preceding line will help the module return a true value