File Coverage

blib/lib/Roku/LCD.pm
Criterion Covered Total %
statement 21 255 8.2
branch 0 70 0.0
condition 0 46 0.0
subroutine 7 22 31.8
pod 6 6 100.0
total 34 399 8.5


line stmt bran cond sub pod time code
1             package Roku::LCD;
2            
3 1     1   24421 use v5.10.1;
  1         5  
  1         60  
4 1     1   13 use strict;
  1         2  
  1         38  
5 1     1   7 use warnings;
  1         15  
  1         41  
6 1     1   1054 use Time::HiRes qw(sleep);
  1         1953  
  1         5  
7 1     1   1079 use Readonly;
  1         3666  
  1         63  
8 1     1   10 use Carp qw(croak);
  1         2  
  1         163  
9            
10             # Constants
11             Readonly::Scalar our $EMPTY => q{};
12             Readonly::Scalar our $SPACE => q{ };
13             Readonly::Scalar our $M400 => 400; # Model type
14             Readonly::Scalar our $M500 => 500; # Model type
15             Readonly::Scalar our $M400WIDTH => 16; # M400 screen width
16             Readonly::Scalar our $M500WIDTH => 40; # M500 screen width
17             Readonly::Scalar our $LETTERPAUSE => 0.25; # Time to pause between printing characters
18            
19             require Roku::RCP;
20            
21 1     1   943 use parent qw(Roku::RCP);
  1         301  
  1         7  
22            
23             our $VERSION = '0.05';
24            
25             =head1 NAME
26            
27             Roku::LCD - M400 & M500 Display Functions made more accessible than via the Roku::RCP module
28            
29             =head1 VERSION
30            
31             =over
32            
33             =item Version 0.05 May 27, 2014 - continuing to modernize the code
34            
35             =back
36            
37             =head1 SYNOPSIS
38            
39            
40             use Roku::LCD;
41             my $display = Roku::LCD->new($rokuIP);
42             if (! display) { die("Could not connect to Roku Soundbridge"); }
43            
44             my($rv) = $display->marquee(text => "This allows easy access to the marquee function - timings for M400 only");
45            
46             $display->ticker(text => "An alternative to the marquee function that can cope with large quantities of text", pause => 5);
47            
48             open (INFILE, "a_text_file.txt");
49             @slurp_file = ;
50             close(INFILE);
51            
52             $display->teletype(text => "@slurp_file", pause => 2, linepause => 1);
53            
54             $display->Quit;
55            
56             =head1 DESCRIPTION
57            
58             Roku::LCD was written because the RokuUI module appeared a bit too high level, so I put together some simplified display
59             routines into a single easy-to-use object.
60            
61             It has now been moved to using the Roku::RCP module which is easily available from CPAN.
62            
63             It inherits all the methods from the standard Roku::RCP module.
64            
65             =head1 METHODS
66            
67             =head2 new(host => I [, port => I] [, model => I<400 or 500>])
68            
69             If not given, the port number is assumed to be 4444, and the model will be determined from the displaytype
70             command (if that fails, the model type will be set to M400).
71            
72             =cut
73            
74             sub new {
75 0     0 1   my ( $class, %args ) = @_;
76 0 0         if (! $args{Host}) { croak "No soundbridge host to control"; }
  0            
77            
78             # Test model type before attempting to connect
79 0 0 0       if ( ( $args{model} ) && ( $args{model} != $M500 ) && ( $args{model} != $M400 ) ) {
      0        
80 0           croak 'Unrecognised model type, ', $args{model}, "\n";
81             }
82            
83             # Roku::RCP really ought to take host within the %args list...
84 0   0       my $self = $class->SUPER::new( $args{Host}, Port => $args{Port} || '4444' );
85            
86 0 0         if (! defined $self) { return; }
  0            
87            
88 0 0         if ( $args{model} ) {
89 0 0         if ( $args{model} == $M500 ) {
    0          
90             # prefer arrow notation to typeglobs used in Roku::RCP
91             # ${*$self}{display_length} = $M500WIDTH ;
92 0           ${$self}->{display_length} = $M500WIDTH ;
  0            
93 0           ${$self}->{model} = $args{model};
  0            
94             }
95             elsif ( $args{model} == $M400 ) {
96 0           ${$self}->{display_length} = $M400WIDTH ;
  0            
97 0           ${$self}->{model} = $args{model};
  0            
98             }
99             }
100             else {
101 0           my $result = $self->_determine_model;
102            
103 0 0         if (! ${$self}->{model}) {
  0            
104 0           croak "Unrecognised display type - unknown model type. Try setting manually.\n";
105             }
106             }
107            
108 0           print " ref \$self = '", ref $self ,"'\n ref \*\$self = '", ref *$self ,"'\n ref \${\$self} = '", ref ${$self} , "'\n ref \${\*\$self} = '", ref ${*$self}, "'\n";
  0            
  0            
109            
110 0 0         if ( ${$self}->{debug} ) {
  0            
111 0           print "DEBUG display length = ${$self}->{display_length}; model = M${$self}->{model}\n";
  0            
  0            
112             }
113            
114 0           return bless $self, $class;
115             } # end new
116            
117             =head2 marquee(text => I [, clear => I<0/1>])
118            
119             This allows quick access to the standard sketch marquee function - timings are for text sized to
120             the M400 display as I do not have access to an M500.
121            
122             If 1 is passed to clear, it forces the display to clear first (default 0)
123            
124             =cut
125            
126             sub marquee {
127 0     0 1   my ( $self, %args ) = @_;
128            
129             # only take over if on standby
130 0 0         if (! $self->onstandby ) {
131 0           return ("Soundbridge running");
132             }
133 0   0       my $text = $args{'text'} || $EMPTY;
134 0   0       my $clear = $args{'clear'} || 0;
135            
136             # duration is a magic number - time to wait before releasing display.
137 0           my $duration = ( int( ( ( length($text) ) + 24 ) / 25 ) ) * 5;
138            
139 0 0         if ( ${$self}->{debug} ) {
  0            
140 0           print "DEBUG text length = ", length($text),
141             " duration = $duration\n";
142             }
143            
144 0 0         if ($clear) { $self->_clear; }
  0            
145 0           $self->command("sketch -c marquee -start \"$text\"");
146 0           sleep($duration);
147 0           $self->command('sketch -c quit');
148 0           $self->command('sketch -c exit');
149            
150 0           return ($self->sb_response);
151             } # end marquee
152            
153            
154             sub _blank_line {
155             # clears a single line
156 0     0     my ( $self, $line ) = @_;
157 0           my $rc = $self->_text(
158             text => $self->_spacefill(text => $SPACE),
159             duration => 0,
160             y => $line
161             );
162 0           return $rc;
163             } # end _blank_line
164            
165            
166             sub _clear {
167             # clear the display
168 0     0     my $self = shift;
169 0           $self->command('sketch -c clear');
170 0           my $rc = $self->sb_response;
171 0           return ($rc);
172             }
173            
174             sub _determine_model {
175             # determine the soundbridge model from the display size
176             # M400 returns "16x2 LCD" - I assume M500 returns "40x2 LCD"
177 0     0     my $self = shift;
178 0           $self->command("displaytype");
179            
180 0           my @responses = $self->sb_response();
181 0           foreach my $response (@responses) {
182            
183 0 0         if ( ${$self}->{debug} ) {
  0            
184 0           print "DEBUG display type returned '$response'\n";
185             }
186 0 0         if ($response =~ /^(\d{2})x/) {
187 0           ${$self}->{display_length} = $1;
  0            
188 0 0         if (${$self}->{display_length} == $M500WIDTH) {
  0            
189 0           ${$self}->{model} = $M500 ;
  0            
190 0           return "model $M500";
191             }
192             else { # assume it's 16
193 0           ${$self}->{model} = $M400 ;
  0            
194 0           return "model $M400";
195             }
196             }
197             }
198 0           return; # nothing appeared - return empty handed
199             } # end _determine_model
200            
201            
202             sub _spacefill {
203             # pad line with spaces - used to overwrite previous lines
204             # WARNING! This is an internal function, and likely to change
205 0     0     my ( $self, %args ) = @_;
206 0   0       my $text = $args{'text'} || $EMPTY;
207 0           my $tl = length($text);
208            
209             # how many spaces do we need ?
210 0           my $spc = ${$self}->{display_length} - $tl;
  0            
211 0 0         if ($spc < 1) {
212             # no padding required
213 0           return $text;
214             }
215             else {
216 0           my $pattern = "%${tl}s%${spc}s";
217 0           return sprintf $pattern, $text, $SPACE;
218             }
219             } # end _spacefill
220            
221             sub _text {
222             # internal function allowing easy access to the sketch "text" command
223             # usage:
224             # _text(text => I , duration => I [, clear => I<0/1>], x => I, y => I<0/1>)
225 0     0     my ( $self, %args ) = @_;
226            
227 0   0       my $text = $args{'text'} || $SPACE;
228 0   0       my $x = $args{'x'} || 0;
229 0   0       my $y = $args{'y'} || 0;
230 0           my $duration = $args{'duration'};
231            
232 0           $self->command("text $x $y \"$text\"");
233 0           sleep($duration);
234 0           return 1;
235             } # end _text
236            
237            
238             sub _print_current_line {
239             # An internal function for the teletype method
240             # clears, then prints the current line
241 0     0     my ( $self, $text, $y ) = @_;
242 0           my $rc = $self->_blank_line($y);
243 0           $rc = $self->_ticker(
244             text => $text,
245             y => $y,
246             pause => $LETTERPAUSE
247             );
248 0           return $rc;
249             } # end _print_current_line
250            
251            
252             sub _print_last_line {
253             # An internal function for the teletype method
254             # prints the last line on the top line
255 0     0     my ( $self, $text ) = @_;
256 0           my $rc = $self->_text(
257             text => $text,
258             duration => 0,
259             y => 0
260             );
261 0           return $rc;
262             } # end _print_last_line
263            
264            
265             sub _ttparagraph {
266             # An internal method which processes individual paragraphs for the teletype method
267 0     0     my ( $self, $text, $last_line_ref, $y_ref ) = @_;
268 0           my $dlength = ${$self}->{display_length}; # width of display
  0            
269 0           my $current_line;
270 0           my $current_line_length = 0;
271 0           my $rc;
272            
273             # is the paragraph small enough to be printed on one line?
274 0 0         if (length($text) <= $dlength) {
275 0 0         if (${$last_line_ref}) {
  0            
276 0           $rc = $self->_print_last_line(${$last_line_ref});
  0            
277             }
278 0           $rc = $self->_print_current_line($text, ${$y_ref});
  0            
279             # start next line
280 0           ${$y_ref} = 1;
  0            
281 0           ${$last_line_ref} = $self->_spacefill(text => $text);
  0            
282             }
283             else {
284             # process the paragraph - break it into words (split on space)
285 0           my @string = split(/ /, $text);
286            
287             # work through each word in the array (ary_inx holds the current word's position)
288 0           foreach my $word (@string) {
289            
290 0 0 0       if ( ( length( $word ) + $current_line_length ) < $dlength ) {
    0          
291             # if the word will fit on the current line
292             # (note less than as a space needs to be accomodated too)
293 0 0         $current_line .= $SPACE if ($current_line);
294 0           $current_line .= $word;
295 0           $current_line_length = length($current_line);
296             }
297             # elsif the word will not fit on the current line but contains a non-word character - split on that (add one to the length because there's a space)
298             elsif ( ( $word =~ /^(\S+\W)(\S+)$/ )
299             && ( ( length($1) + $current_line_length + 1 ) < $dlength ) )
300             {
301 0 0         if ($current_line) { $current_line .= $SPACE; }
  0            
302 0           $current_line .= $1;
303             # print the line
304 0 0         if (${$last_line_ref}) {
  0            
305 0           $rc = $self->_print_last_line(${$last_line_ref});
  0            
306             }
307 0           $rc = $self->_print_current_line($current_line, ${$y_ref});
  0            
308             # start next line
309 0           ${$y_ref} = 1;
  0            
310 0           ${$last_line_ref} = $self->_spacefill(text => $current_line);
  0            
311 0           $current_line = $2;
312 0           $current_line_length = length($current_line);
313             }
314             else {
315             # too big for line, so print the line
316 0 0         if (${$last_line_ref}) {
  0            
317 0           $rc = $self->_print_last_line(${$last_line_ref});
  0            
318             }
319 0           $rc = $self->_print_current_line($current_line, ${$y_ref});
  0            
320             # start next line
321 0           ${$y_ref} = 1;
  0            
322 0           ${$last_line_ref} = $self->_spacefill(text => $current_line);
  0            
323 0           $current_line = $word;
324 0           $current_line_length = length($current_line);
325             }
326             } # end foreach @string loop
327             # we've run out of words, but we haven't printed the line yet!
328 0 0         if (${$last_line_ref}) {
  0            
329 0           $rc = $self->_print_last_line(${$last_line_ref});
  0            
330             }
331 0           $rc = $self->_print_current_line($current_line, ${$y_ref});
  0            
332             # fill last line for next paragraph call
333 0           ${$y_ref} = 1;
  0            
334 0           ${$last_line_ref} = $self->_spacefill(text => $current_line);
  0            
335             } # end paragraph processing
336 0           return $rc;
337             } # end _ttparagraph
338            
339            
340             =head2 ticker(text => I [, y => I<0/1>] [, pause => I])
341            
342             An alternative to the marquee that can be displayed on either the top or bottom line.
343            
344             =cut
345            
346             sub ticker { # an alternative to marquee
347 0     0 1   my ( $self, %args ) = @_;
348             # only take over if on standby
349 0 0         if (! $self->onstandby ) {
350 0           return ('Soundbridge running');
351             }
352            
353 0           $self->command('sketch');
354            
355 0           $self->_ticker(%args);
356            
357 0           $self->command('quit');
358 0           my $rc = $self->sb_response;
359 0           return ($rc);
360             } # end ticker
361            
362            
363             sub _ticker { # the real function - also used by teletype
364 0     0     my ( $self, %args ) = @_;
365 0   0       my $text = $args{'text'} || $EMPTY;
366 0   0       my $pause = $args{'pause'} || 5;
367 0   0       my $y = $args{'y'} || 0;
368 0           my $dlength = ${$self}->{display_length};
  0            
369 0           my $offset = 0; # offset for taking a substring
370 0           my $dtext = '0'; # currently displayed text
371 0           my $tlength = 0; # length of currently displayed text
372 0           my $dur = 0;
373 0           my $spc = 0;
374            
375 0           my $length = 0;
376 0           while(++$length < ( length($text) ) ) {
377 0           $spc++;
378 0 0         if ( $tlength != $dlength ) {
379             # current text length != display width
380 0           $tlength++;
381             }
382            
383 0 0         if ( length($dtext) == $dlength ) {
384             # increase the offset if the displayed text is the same length as the screen width
385 0           $offset++;
386             }
387            
388 0           $dtext = substr( $text, $offset, $tlength );
389 0 0         if ( substr( $dtext, -1, 1 ) eq $SPACE ) { $spc = 0; }
  0            
390            
391 0 0 0       if ( ( length($text) > $dlength ) && ( ++$dur == $dlength ) ) {
392             # print "length > dlength && dur == dlength\n";
393 0           $self->_text( text => $dtext, duration => $LETTERPAUSE, y => $y );
394 0 0         if ( ${$self}->{debug} ) {
  0            
395 0           print "DEBUG dtext='$dtext' dur='$dur' spc='$spc'\n";
396             }
397 0           $dur = $spc;
398 0 0         if ( $dur > $dlength ) { $dur = 0; }
  0            
399             }
400             else {
401             # print "length <= dlength || dur != dlength\n";
402 0           $self->_text( text => $dtext, duration => $LETTERPAUSE, y => $y );
403 0 0         if ( ${$self}->{debug} ) {
  0            
404 0           print "DEBUG dtext='$dtext' dur='$dur' spc='$spc'\n";
405             }
406             }
407             }
408 0           $dtext = substr( $text, -$dlength, $dlength );
409 0           $self->_text( text => $dtext, duration => $pause, y => $y );
410 0           return 1;
411             } # end _ticker
412            
413             =head2 teletype(text => I [, pause => I] [, [linepause => I])
414            
415             An alternative to using marquee to display large quantities of text, scrolling the display upwards rather than from
416             the right.
417            
418             The length of time to pause after each line of text is given by I, wheras I holds the
419             length of time to pause at the end of the text.
420            
421             =cut
422            
423             sub teletype {
424 0     0 1   my ( $self, %args ) = @_;
425 0   0       my $text = $args{'text'} || $EMPTY; # default text is blank
426 0   0       my $linepause = $args{'linepause'} || 1; # length of time to wait in seconds before next line
427 0   0       my $pause = $args{'pause'} || 1; # length of additional time to wait in seconds after message
428            
429             # only take over if on standby
430 0 0         if (! $self->onstandby ) {
431 0           return ("Soundbridge running");
432             }
433            
434 0           $self->command('sketch'); # put the command session into sketch mode
435            
436             # Clear display first
437 0           $self->_clear;
438            
439 0           my @string;
440             my $rc; # message returned by method
441 0           my $dlength = ${$self}->{display_length}; # width of display
  0            
442 0           my $line_length = 0; # current length of line
443 0           my $y = 0; # start at the top
444 0           my $last_string = undef; # last string printed
445            
446 0           my (@paras) = split( /\n/, $text ); # break the text into paragraphs
447 0           foreach my $paragraph (@paras) {
448 0           $self->_ttparagraph($paragraph, \$last_string, \$y)
449             }
450 0           $rc = $self->_print_last_line($last_string);
451 0           $rc = $self->_text(
452             text => $self->_spacefill( text => $SPACE ),
453             duration => 0,
454             y => 1
455             );
456 0           sleep($pause);
457 0           $self->command('quit');
458 0           $rc = $self->sb_response;
459 0           return ($rc);
460             } # end teletype
461            
462             =head2 onstandby
463            
464             Checks whether the Soundbridge is on standby (returns true) or in use (returns false)
465            
466             =cut
467            
468             sub onstandby {
469            
470             # an almost direct lift of RokuUI's ison function
471             # this is used to see whether the radio is in use
472 0     0 1   my $self = shift;
473 0           $self->command("ps");
474            
475 0           for my $ps ( $self->sb_response ) {
476 0 0         return 1 if $ps =~ /StandbyApp/;
477             }
478 0           return 0;
479             } # end onstandby
480            
481             =head2 sb_response
482            
483             Used to return any command responses; filtering out prompts
484            
485             =cut
486            
487             sub sb_response {
488            
489             # this is used to return any command responses, but filter out prompts
490 0     0 1   my $self = shift;
491             return map {
492 0 0 0       if ( ( !/^SoundBridge\>/ ) && ( !/^Sketch>/ ) ) { $_; }
  0            
  0            
493             } $self->response();
494             } # end sb_response
495            
496             1;
497            
498             # end of module, additional documentation below
499            
500             __END__