File Coverage

blib/lib/Audio/Radio/Sirius.pm
Criterion Covered Total %
statement 67 388 17.2
branch 13 166 7.8
condition 0 36 0.0
subroutine 12 32 37.5
pod 9 9 100.0
total 101 631 16.0


line stmt bran cond sub pod time code
1             package Audio::Radio::Sirius;
2            
3 3     3   1545735 use 5.008;
  3         11  
  3         147  
4            
5 3     3   96 use warnings;
  3         7  
  3         97  
6 3     3   18 use strict;
  3         19  
  3         116  
7            
8 3     3   17 use Carp;
  3         6  
  3         255  
9 3     3   8473 use Time::HiRes qw(sleep); # need to sleep for milliseconds in some receive loops
  3         7507  
  3         18  
10            
11             =head1 NAME
12            
13             Audio::Radio::Sirius - Control a Sirius satellite radio tuner
14            
15             =head1 VERSION
16            
17             Version 0.03
18            
19             =cut
20            
21             our $VERSION = '0.03';
22             our $AUTOLOAD;
23            
24             our %DEFAULTS = (
25             power => 0,
26             connected => 0,
27             channel => 0,
28             gain => 0,
29             debug => 0,
30             mute => 0,
31             verbosity => 0,
32             _sequence => 0,
33             _serial => undef,
34             _lastack => -1,
35             _lastreq => -1,
36             _callbacks => {
37             'channel_update' => undef,
38             },
39             _buffer => '',
40             );
41            
42             our %SETTABLE = (
43             debug => 1,
44             );
45            
46             our %COMMANDS = (
47             poweroff => '000800',
48             reset => '0009',
49             poweron => '000803',
50             volume => '0002',
51             mute => '0003',
52             channel => '000a', channel_suffix => '000b',
53             request_signal => '4018',
54             request_unkn1 => '4017',
55             request_sid => '4011',
56             verbosity => '000d000000'
57             );
58            
59             our %UPDATES = (
60             '2008' => {
61             name => 'power',
62             handler => undef
63             },
64             '2002' => {
65             name => 'volume',
66             handler => undef
67             },
68             '2003' => {
69             name => 'mute',
70             handler => undef
71             },
72             '200a' => {
73             name => 'channel',
74             handler => \&_channel_update,
75             removefirst => 4
76             },
77             '200d' => {
78             name => 'verbosity',
79             handler => undef
80             },
81             '6011' => {
82             name => 'reply_sid',
83             handler => undef
84             },
85             '6017' => {
86             name => 'reply_unkn1',
87             handler => undef
88             },
89             '6018' => {
90             name => 'reply_signal',
91             handler => undef
92             },
93             '8001' => {
94             name => 'channel_info',
95             handler => \&_channel_item_update,
96             removefirst => 2
97             },
98             '8002' => {
99             # The way verbosity works now, we won't see PID info. Verbosity must not include channel updates or it only sends those
100             # (mostly because PIDs are part of channel updates).
101             name => 'pid_info',
102             handler => undef,
103             },
104             '8003' => {
105             name => 'time_info',
106             handler => \&_time_update,
107             removefirst => 2
108             },
109             '8004' => {
110             # 1 1 0 - acquiring signal
111             # 1 0 0 - all's well
112             # 2 1 0 - antenna disconnected
113             # 2 0 1 - antenna back
114             name => 'tuner_info',
115             handler => undef,
116             removefirst => 2
117             },
118             '8005' => {
119             name => 'signal_info',
120             handler => \&_signal_update,
121             removefirst => 2
122             },
123             );
124            
125             our %TYPES = (
126             command => '00',
127             ack => '80',
128             e_busy => '82',
129             e_checksum => '83'
130             );
131            
132             our %ITEM_TYPES = (
133             0x1 => 'artist',
134             0x2 => 'title',
135             0x6 => 'composer',
136             0x86 => 'pid'
137             );
138            
139            
140             our $START = 'a40300'; # Const that prefaces each command
141            
142             =head1 SYNOPSIS
143            
144             Sirius satellite radio (L) is a US based satellite radio serice. While none of the tuners they make have serial or USB connectors,
145             it has been found that generation 2.5 tuners (Sportster, Starmate, * Replay, Sirius Connect, and others) have a common tuner module. Furthermore
146             this tuner module generally has a serial interface. Presently only one commercial site is offering a modification for adding a serial port to a
147             Sirius tuner: L. Google should reveal schematics and parts needed for adding ports to other tuners.
148            
149             Once your tuner is connected to your system and accessible via a serial port like device, you can use this module to access it:
150            
151             use Audio::Radio::Sirius;
152             use Win32::SerialPort; # or Device::SerialPort on Linux
153            
154             my $serial = new Win32::SerialPort('com1');
155             my $tuner = new Audio::Radio::Sirius;
156            
157             $tuner->connect($serial);
158             $tuner->power(1);
159             $tuner->channel(184); # tune in the preview channel
160            
161             =head1 CONSTRUCTOR
162            
163             =head2 new
164            
165             Call new to create an instance of the Sirius radio object. Once the object is created, you will probably want to L to it.
166            
167             =cut
168            
169             sub new {
170 2     2 1 20 my $class = shift;
171 2         18 my $self = { %DEFAULTS };
172 2         34 bless $self, $class;
173 2         8 return $self;
174             }
175            
176             sub AUTOLOAD {
177 0     0   0 my $self = shift;
178 0 0       0 my $type = ref($self) or croak "$self is not an object";
179            
180 0         0 my $name = $AUTOLOAD;
181 0         0 $name =~ s/.*://; # Remove Audio::Radio::Sirius:: bit
182            
183 0 0       0 unless (exists $self->{$name}) { croak "$name is not a field in class $type"; }
  0         0  
184            
185 0 0       0 if (@_) {
186             # setter
187 0 0       0 if (defined($SETTABLE{$name}) ) { return $self->{$name} = shift; }
  0         0  
188 0         0 else { croak "$name cannot be changed."; }
189             } else {
190 0         0 return $self->{$name};
191             }
192             }
193            
194             sub DESTROY {
195 2     2   3279 my $self = shift;
196            
197 2 50       7856 if (defined($self->{_serial} )) {
198 0         0 $self->{_serial}->close;
199 0         0 undef $self->{_serial};
200             }
201             }
202            
203             =head1 OBJECT METHODS
204            
205             =head2 connect (serialport object)
206            
207             Connect establishes a connection between the tuner object and the SerialPort object. The SerialPort
208             object must be a Win32::SerialPort or a Device::SerialPort.
209            
210             require Win32::SerialPort;
211            
212             my $serial_port = new Win32::SerialPort('com1');
213            
214             $tuner->connect($serial_port);
215            
216             =cut
217            
218             sub connect {
219 0     0 1 0 my $self = shift;
220 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
221 0         0 my ($connection) = @_;
222 0         0 my $connectiontype = ref($connection);
223            
224             ### TODO: switch to isa() here to allow derived classes
225 0 0 0     0 if (($connectiontype eq "Win32::SerialPort")
226             || ($connectiontype eq "Device::SerialPort")) {
227 0         0 $connection->baudrate(57600);
228 0         0 $connection->parity('none');
229 0         0 $connection->databits(8);
230 0         0 $connection->stopbits(1);
231 0         0 $connection->handshake('none');
232             # $connection->read_const_time(150);
233             # $connection->read_interval(50);
234             # $connection->read_char_time(10);
235             # $connection->write_char_time(10);
236             # $connection->read_const_time(1000);
237             # $connection->read_interval(5);
238             # $connection->read_char_time(50);
239             # $connection->write_char_time(0);
240 0 0       0 if (!$connection->write_settings) {
241 0         0 carp "Couldn't open connection: $_";
242 0         0 return 0;
243             }
244 0         0 $self->{_serial} = $connection;
245             # $self->_send_command($COMMANDS{'reset'});
246             # if ( !$self->_send_command($COMMANDS{'poweroff'}) ) {
247             # carp "Tuner didn't respond to poweroff command";
248             # return 0;
249             # }
250 0         0 $self->{connected} = 1; # we're live
251 0         0 return 1;
252             } else {
253 0         0 croak "Connect needs a Win32::SerialPort or a Device::SerialPort, got a $connectiontype";
254             }
255             }
256            
257             =head2 power (state)
258            
259             Use to turn the radio on (1) or off (0). Returns true if succeeded.
260            
261             $tuner->power(1); # Power on tuner.
262            
263             =cut
264            
265             sub power {
266             ### TODO: Needs accessor and turn off method
267 0     0 1 0 my $self = shift;
268 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
269 0         0 my ($powerreq) = @_;
270            
271 0 0       0 if (!defined($powerreq)) { return $self->{'power'}; }
  0         0  
272 0 0       0 if ($powerreq == 1) {
273 0         0 my $current_gain = $self->{gain};
274 0         0 my $current_mute = $self->{mute};
275 0 0 0     0 if (!(
276             $self->_send_command($COMMANDS{'reset'}) &&
277             $self->_send_command($COMMANDS{'poweroff'}) &&
278             $self->_send_command($COMMANDS{'poweron'}) &&
279             # $self->_send_command('000c0000001700') && #useless
280             $self->gain($current_gain) &&
281             $self->_send_command($COMMANDS{'request_signal'}) &&
282             $self->_send_command($COMMANDS{'request_sid'}) &&
283             $self->mute($current_mute)
284             # $self->{'power'} = 1
285             )) {
286 0         0 carp "Error - tuner failed to respond to power-up sequence.";
287 0         0 return 0;
288             }
289             } else {
290 0         0 $self->_send_command($COMMANDS{'poweroff'});
291 0         0 $self->{'power'} = 0;
292             }
293             }
294            
295             =head2 gain (db)
296            
297             Gain ranges from -9db to 0db. It defaults to 0. When called with a parameter, gain returns
298             false on failure and true on success. When called without a parameter, gain returns the current gain
299             setting.
300            
301             $tuner->gain(-6); # Mom's on the phone, turn down Howard Stern
302            
303             my $current_gain = $tuner->gain;
304            
305             =cut
306            
307             sub gain {
308 0     0 1 0 my $self = shift;
309 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
310            
311 0         0 my ($gainreq) = @_;
312            
313 0 0       0 if (!defined($gainreq)) { return $self->{gain}; } # accessor
  0         0  
314            
315             # mutator
316 0 0 0     0 if (!($gainreq <= 0) && ($gainreq >= -9)) {
317 0         0 carp "Requested gain out of range: $gainreq. Must be between -9 and 0.";
318 0         0 return 0;
319             }
320            
321 0         0 my $gainhex = $self->_num_to_signed_hex($gainreq);
322 0         0 my $cmd = $COMMANDS{volume}.$gainhex;
323            
324 0 0       0 if (!$self->_send_command($cmd)) {
325 0         0 carp "Tuner did not respond to gain setting.";
326 0         0 return 0;
327             }
328 0         0 return 1;
329             }
330            
331             =head2 mute (mute setting)
332            
333             When called with a parameter, you can set it to 1 to mute and 0 to unmute. Called without a parameter
334             retrieves the current setting.
335            
336             my $result = $tuner->mute(0); # Unmute the tuner
337            
338             my $muted = $tuner->mute;
339            
340             =cut
341            
342             sub mute {
343 0     0 1 0 my $self = shift;
344 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
345            
346 0         0 my ($mutereq) = @_;
347            
348 0 0       0 if (!defined($mutereq)) { return $self->{mute}; } # accessor
  0         0  
349            
350 0 0 0     0 if (!( ($mutereq == 0) || ($mutereq == 1) ) ) {
351 0         0 carp "Mute must be either 0 or 1.";
352 0         0 return 0;
353             }
354            
355 0         0 my $mutehex = $self->_num_to_signed_hex($mutereq);
356 0         0 my $cmd = $COMMANDS{'mute'} . $mutehex;
357 0 0       0 if (!$self->_send_command($cmd)) {
358 0         0 carp "Tuner did not respond to mute command.";
359 0         0 return 0;
360             }
361            
362 0         0 return 1;
363             }
364            
365             =head2 channel (channel number, offset)
366            
367             Can be used without a parameter to get the current channel number or with a parameter to change channels. When used with a parameter, returns true
368             on success and false on failure. Offset is -1 to select the channel before the specified number, 1 to select the channel above the specified number,
369             or 0 (default) to simply go to the specified channel.
370            
371             my $current_channel = $tuner->channel;
372            
373             my $result = $tuner->channel(6, 1); # Tune to channel 7
374            
375             $tuner->channel(100); # Tune directly to channel 100
376            
377             =cut
378             sub channel {
379 0     0 1 0 my $self = shift;
380 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
381            
382 0         0 my ($chanreq, $offsetreq) = @_;
383 0         0 my $offset = 0;
384            
385 0 0       0 if (!defined($chanreq)) { return $self->{channel}; } # accessor
  0         0  
386 0 0 0     0 if (defined($offsetreq) && ($offsetreq =~ /0|1|-1/) ) { $offset = $offsetreq; }
  0         0  
387            
388             ### TODO: Channel validation.
389            
390             # channel command: $COMMAND, channel, [0,1,-1], $COMMAND suffix
391 0         0 my $chanhex = $self->_num_to_unsigned_hex($chanreq);
392 0         0 my $offsethex = $self->_num_to_signed_hex($offset);
393 0         0 my $cmd = $COMMANDS{channel} . $chanhex . $offsethex . $COMMANDS{channel_suffix};
394 0         0 return $self->_send_command($cmd);
395             }
396            
397             =head2 monitor (cycles)
398            
399             Monitor is called to watch for updates from the tuner. The Sirius tuner is pretty chatty and sends relevant data, such as Artist/Title updates,
400             PIDs, signal strength, and other information. Calling monitor initiates reads of this data.
401            
402             Reads happen automatically when commands are executed (for example changing the channel or muting the tuner). Still, monitor generally needs
403             to be called as often as possible to gather the latest data from the Tuner.
404            
405             A monitor cycle will take a minimum of one second. If data is received, this timer resets. In other words, monitor may take longer than you anticipate.
406             The amount of time monitor takes will depend on the C of the tuner.
407            
408             If no number of cycles is specified, monitor runs one cycle.
409            
410             B As of version 0.02, the cycle parameter is no longer a true count of the number of cycles. The number specified is multiplied by 20.
411             Each cycle now sleeps 50 msec so the result is roughly the same, although this may increase the drift of cycles vs. seconds even more.
412            
413             $tuner->monitor(5); # spin 5 times
414            
415             =cut
416            
417             sub monitor {
418 0     0 1 0 my $self = shift;
419 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
420            
421 0         0 my ($spins) = @_;
422            
423 0 0       0 if (!defined($spins)) { $spins = 1; }
  0         0  
424 0         0 $spins = $spins * 20;
425 0         0 foreach (1..$spins) {
426 0         0 $self->_receive_if_waiting;
427 0         0 sleep (.05); # chill .05 second
428             }
429             }
430            
431             =head2 set_callback (callback type, function reference)
432            
433             When the tuner sends an update, such as new artist/title information on the current channel, it may be helpful to execute some code which handles this
434             event. To accomidate this, you may define function callbacks activated when each event occurs. Note that some of the parameters below are marked with
435             an asterisk. This indicates that they may be undefined when your function is called. You should account for this in your callback function.
436            
437             =head3 channel_update (channel, *pid, *artist, *title, *composer)
438            
439             $tuner->set_callback ('channel_update', \&channel);
440            
441             sub channel {
442             my ($channel, $pid, $artist, $title, $composer) = @_;
443             print "Channel $channel is now playing $title.\n";
444             }
445            
446             =head3 signal_update
447            
448             Not yet implemented.
449            
450             =head3 time_update
451            
452             Not yet implemented.
453            
454             =head3 status_update
455            
456             Not yet implemented.
457            
458             =cut
459            
460             sub set_callback {
461 1     1 1 7 my $self = shift;
462 1 50       7 if (!ref($self) eq 'CODE') { croak "$self isn't an object"; }
  0         0  
463 1         2 my ($reqtype, $funcref) = @_;
464 1 50       5 if (!ref $funcref) { croak "$funcref must be a reference to a function"; }
  0         0  
465 1 50       7 if (!exists($DEFAULTS{'_callbacks'}{$reqtype}) ) { croak "$reqtype is not a supported update type"; }
  0         0  
466             # validated enough for 'ya??
467            
468 1         12 $self->{'_callbacks'}{$reqtype} = $funcref;
469             }
470            
471             =head2 verbosity (level)
472            
473             Not to be confused with C, verbosity changes the updates the tuner sends. By default, the tuner only sends updates for artist/title/PID
474             on the current channel. The Generation 2.5 tuners can send artist/title on all channels, the current time, signal strength, and PID information on all
475             channels.
476            
477             Internally the tuner treats verbosity as a bitmap allowing you to control each type of update you are interested in. For now, this module treats it
478             as a boolean. 0 (default) requests that no updates be sent. 1 requests that all of the following updates are sent:
479            
480             =over
481            
482             =item *
483            
484             Artist/Title information for every channel
485            
486             =item *
487            
488             PID information for every channel
489            
490             =item *
491            
492             Signal strength
493            
494             =item *
495            
496             Current time
497            
498             =back
499            
500             $tuner->verbosity(1); #request all of these updates
501             $current_verbosity=$tuner->verbosity;
502            
503             =cut
504            
505             sub verbosity {
506 0     0 1 0 my $self = shift;
507 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
508 0         0 my ($verbreq) = @_;
509            
510 0 0       0 if (!defined($verbreq)) { return $self->{verbosity}; } # accessor
  0         0  
511 0 0       0 if ($verbreq == 0) {
512             # 0 = no verbosity, 1b = full verbosity
513 0         0 my $cmd = $COMMANDS{verbosity}.'0000';
514 0         0 $self->_send_command($cmd);
515 0         0 $self->{verbosity} = $verbreq;
516             }
517 0 0       0 if ($verbreq == 1) {
518             # 0 = no verbosity, 1b = full verbosity
519             # my $cmd = $COMMANDS{verbosity}.'1b00';
520 0         0 my $cmd = $COMMANDS{verbosity}.'1f00';
521 0         0 $self->_send_command($cmd);
522 0         0 $self->{verbosity} = $verbreq;
523             }
524             }
525            
526             sub _read {
527             # _read works like read from $serial. except better.
528             # returns ($count, $data)
529             # the tests for > 200000 check for the get_tick_count function wrapping
530             # (happens every 43 days or something)
531 0     0   0 my $self = shift;
532 0         0 my ($count) = @_;
533 0         0 my $debug = $self->{debug};
534 0         0 my $serial = $self->{_serial};
535 0         0 my $buffer = $self->{_buffer};
536 0         0 my $buffer_count = length($buffer);
537            
538 0         0 my $data = '';
539 0         0 my $data_count = 0;
540            
541 0         0 my $timeout = 100;
542 0         0 my $start_ticks = $serial->get_tick_count;
543 0         0 my $end_ticks = $start_ticks + $timeout;
544 0   0     0 WAIT: while ( (($serial->status)[1] == 0) && ($buffer_count==0) ) { # loop while nothing is waiting
545 0 0 0     0 if (($serial->get_tick_count > $end_ticks) || (($end_ticks - $serial->get_tick_count) > 200000)) {
546             # last WAIT;
547 0         0 return 0, $data;
548             }
549 0         0 sleep .005;
550             #print "hi $buffer_count\n";
551             }
552            
553             # READ: while (($serial->status)[1] > 0) { # loop while data is waiting
554 0   0     0 do {
      0        
555 0         0 my $input = '';
556 0 0       0 if ($buffer_count > 0) {
557 0         0 $input = $buffer;
558 0         0 $self->{_buffer} = '';
559 0         0 $buffer_count = 0;
560             }
561 0         0 $input .= $serial->input;
562 0         0 my $input_count = length($input);
563 0 0       0 if ($input_count > 0) {
564 0         0 $data .= $input;
565 0         0 $data_count += $input_count;
566 0         0 $end_ticks += 6; # bonus delay because we got something
567             }
568 0         0 sleep .001;
569             #print "$data_count: $count\n";
570             } until (($data_count >= $count) || ($serial->get_tick_count > $end_ticks) ||
571             (($end_ticks - $serial->get_tick_count) > 200000));
572            
573 0 0       0 if ($data_count > $count) {
574 0         0 $self->{_buffer} = substr($data, $count);
575 0         0 return $count, substr($data, 0, $count);
576             }
577             #print "returning: $data\n";
578 0         0 return $data_count, $data;
579             }
580            
581             sub _receive_if_waiting {
582 0     0   0 my $self = shift;
583 0 0       0 if (!ref($self)) { croak "$self isn't an object"; }
  0         0  
584            
585 0         0 my $serial = $self->{_serial};
586 0         0 my $waiting = ($serial->status)[1];
587 0 0 0     0 if (defined($waiting) && $waiting > 6) { $self->_receive; }
  0         0  
588             }
589            
590             sub _receive {
591 0     0   0 my $self = shift;
592 0         0 my $serial = $self->{_serial};
593 0         0 my $debug = $self->{debug};
594 0         0 READ: while (1) {
595             #my ($headercount, $header) = $serial->read(6);
596 0         0 my ($headercount, $header) = $self->_read(6);
597 0 0       0 last READ if ($headercount == 0);
598 0 0       0 if ($headercount < 6) {
599 0 0       0 if ($debug) {
600 0         0 my $hexheader = $self->_pformat($header);
601 0         0 print "Read error: headercount is $headercount: $hexheader\n";
602             }
603 0         0 next READ;
604             }
605            
606             # handle escape escape in header (mostly)
607 0         0 my $headerescapes = $header =~ s/\x1b\x1b/\x1b/g;
608 0 0       0 if ($headerescapes) {
609             # read even more
610 0 0       0 if ($debug) { print "Fixing $headerescapes escape characters in header.\n"; }
  0         0  
611             #my ($headercount2, $header2) = $serial->read($headerescapes);
612 0         0 my ($headercount2, $header2) = $self->_read($headerescapes);
613 0 0       0 next READ if ($headercount2 < $headerescapes); # :(
614 0         0 $header .= $header2;
615             }
616            
617 0         0 my ($start, $seq, $type, $length) = unpack('H6C1H2C1', $header);
618            
619 0 0       0 next READ if ($start ne $START); # oy
620            
621             # there's a special case that happens if length = 1b (the escape character). we need to read 1 just to flush it.
622 0 0       0 if ($length == 0x1b) {
623 0 0       0 if ($debug) { print "Length 1b. Flushing 1 character.\n"; }
  0         0  
624             #$serial->read(1);
625 0         0 $self->_read(1);
626             }
627            
628             #my ($datacount, $data) = $serial->read($length+1); # read data and checksum
629 0         0 my ($datacount, $data) = $self->_read($length+1); # read data and checksum
630 0 0       0 next READ if ($datacount < $length + 1); # shouldn't happen
631             # everything was read.
632             # handle the escape character in the data sequence. must be done before checksum.
633 0         0 my $escapecount = $data =~ s/\x1b\x1b/\x1b/g;
634 0 0       0 FIXESC: if ($escapecount) {
635             # read even more
636 0 0       0 if ($debug) { print "Fixing $escapecount escape characters.\n"; }
  0         0  
637             #my ($datacount2, $data2) = $serial->read($escapecount);
638 0         0 my ($datacount2, $data2) = $self->_read($escapecount);
639 0 0       0 next READ if ($datacount2 < $escapecount); # :(
640 0         0 $data .= $data2;
641 0         0 $escapecount = $data =~ s/\x1b\x1b/\x1b/g;
642 0 0       0 if ($escapecount) { redo FIXESC; } # for the special times when we read more data due to escape chars and the data we read contains them... ugh
  0         0  
643             }
644 0 0       0 if ($debug >= 3) {print '<< '.$self->_pformat($header . $data)."\n"; }
  0         0  
645 0         0 my $checksum = chop $data;
646 0         0 my $calculated = $self->_checksum($header . $data);
647 0 0       0 if ($calculated ne $checksum) {
648 0         0 my ($calcval, $realval) = (ord($calculated), ord($checksum) );
649 0 0       0 if ($debug) { print "Checksum didn't match - calc: $calcval act: $realval\n"; }
  0         0  
650 0         0 $self->_send_checksum_error($seq);
651 0         0 next READ; # this is also bad news :(
652             }
653            
654             # start processing for real
655 0 0       0 if ($type eq $TYPES{ack}) {
656 0         0 $self->{_lastack} = $seq;
657 0 0       0 if ($debug) { print "Got an ack for seq: $seq\n"; }
  0         0  
658 0         0 next READ;
659             }
660            
661             # ack it now before we go further. the tuner is impatient.
662 0         0 $self->_send_ack($seq);
663            
664 0 0       0 if ($type eq $TYPES{command}) {
665             # did we get this already?
666 0 0       0 if ($seq == $self->{_lastreq}) {
667             # Tuner is repeating itself... This is bad.
668 0 0       0 if ($debug > 2) { print "Not handling duplicate update seq $seq\n"; }
  0         0  
669 0         0 next READ;
670             }
671 0         0 $self->{_lastreq} = $seq;
672             # handle the update, then send an ack
673 0         0 my $updatetype = unpack ('H4', $data);
674 0 0       0 if (defined($UPDATES{$updatetype})) {
675             # OK... I recognize this update.
676 0         0 my $updatename = $UPDATES{$updatetype}{name};
677 0         0 my $updatehandler = $UPDATES{$updatetype}{handler};
678 0 0       0 if ($debug) {
679 0         0 print "Received an update: $updatename\n";
680             }
681 0 0       0 if (defined($updatehandler)) {
682             # some responses are identical but the identical part starts
683             # somewhere after the command... chop it off to the identical bits
684 0         0 my $removefirst = $UPDATES{$updatetype}{removefirst};
685 0         0 $data=substr($data,$removefirst);
686 0         0 $self->$updatehandler($data);
687             }
688            
689             } else {
690             # unknown command.
691 0 0       0 if ($debug) {
692 0         0 my $datahex = $self->_pformat($data);
693 0         0 print "Unknown update: $updatetype data: $datahex\n";
694             }
695             }
696             }
697             }
698             }
699            
700             sub _channel_update {
701 0     0   0 my $self = shift;
702 0         0 my ($data) = @_;
703            
704 0         0 my ($channel, $categorynum, $shortchan, $longchan, $shortcat, $longcat);
705 0         0 ($channel, $categorynum, $shortchan, $longchan, $shortcat, $longcat, $data) = unpack ('C1xC1xxC1/aC/aC/aC/aa*', $data);
706            
707 0         0 $self->{channel} = $channel;
708            
709 0         0 $self->{categories}{$categorynum}{longname} = $longcat;
710 0         0 $self->{categories}{$categorynum}{shortname} = $shortcat;
711 0         0 $self->{channels}{$channel}{longname} = $longchan;
712 0         0 $self->{channels}{$channel}{shortname} = $shortchan;
713            
714 0         0 $self->{channels}{$channel}{category} = $self->{categories}{$categorynum};
715 0         0 $self->{categories}{$categorynum}{channels}{$channel} = $self->{channels}{$channel};
716            
717             # process left over items
718 0         0 $self->_channel_items($channel, $data);
719            
720             # call handler
721 0         0 $self->_call_channel_handler($channel);
722             }
723            
724             sub _call_channel_handler {
725 2     2   4 my $self = shift;
726 2         4 my ($channel) = @_;
727            
728             # update handler: ($channel, $pid, $artist, $title, $composer)
729 2         5 my $handler = $self->{'_callbacks'}{'channel_update'};
730 2 50       8 if (ref($handler)) {
731 2         13 &$handler (
732             $channel,
733             $self->{'channels'}{$channel}{'pid'},
734             $self->{'channels'}{$channel}{'artist'},
735             $self->{'channels'}{$channel}{'title'},
736             $self->{'channels'}{$channel}{'composer'}
737             );
738             }
739             }
740            
741             sub _signal_update {
742 0     0   0 my $self = shift;
743 0         0 my ($data) = @_;
744 0         0 my $debug = $self->{debug};
745            
746 0         0 my ($overall, $sat, $terrestrial) = unpack ('CCC', $data);
747            
748 0         0 foreach my $signal ($overall, $sat, $terrestrial) {
749 0         0 $signal = $signal * .33;
750             }
751 0 0       0 if ($debug>1) { print "Signal overall: $overall Sat: $sat Terrestrial: $terrestrial\n"; }
  0         0  
752 0         0 $self->{signal}{overall} = $overall;
753 0         0 $self->{signal}{sat} = $sat;
754 0         0 $self->{signal}{terrestrial} = $terrestrial;
755             }
756            
757             sub _time_update {
758 0     0   0 my $self = shift;
759 0         0 my ($data) = @_;
760 0         0 my $debug = $self->{debug};
761            
762 0         0 my ($year, $month, $day, $hour, $minute, $second) = unpack ('nCCCCC', $data);
763 0 0       0 if ($debug>1) { print "Time update: $year-$month-$day $hour:$minute:$second\n"; }
  0         0  
764            
765             # send to user functions as reverse list to conform with perl custom
766             }
767            
768             sub _channel_item_update {
769 2     2   6119 my $self = shift;
770 2         4 my ($data) = @_;
771            
772 2         3 my $channel;
773 2         24 ($channel, $data) = unpack ('C1a*', $data);
774 2         8 $self->_channel_items($channel, $data);
775            
776             # call handler
777 2         6 $self->_call_channel_handler($channel);
778             }
779            
780             sub _channel_items {
781             # multiple updates contain this stuff. call this with $chan and $data.
782 2     2   3 my $self = shift;
783 2         5 my ($channel, $data) = @_;
784 2         5 my $debug=$self->{debug};
785            
786            
787 2         2 my $numitems;
788 2         6 ($numitems, $data) = unpack ('C1a*', $data);
789 2 50       8 if ($numitems>0) {
790             # there be items here
791             # step 1 - clean out the old items
792 2         6 foreach my $clean (values %ITEM_TYPES) {
793 8         23 $self->{channels}{$channel}{$clean} = undef;
794             }
795            
796 2         7 ITEM: foreach (1..$numitems) {
797 9         12 my ($itemtype, $item, $typevar);
798 9         37 ($itemtype, $item, $data) = unpack ('C1C1/aa*', $data);
799 9         20 $typevar = $ITEM_TYPES{$itemtype};
800 9 50       21 if ($debug > 1) { print "Item type: $itemtype Info: $item\n"; }
  0         0  
801 9 100       22 if (!defined($typevar)) {
802 2 50       7 if ($debug) { print "Channel update contained unrecognized item: $itemtype: $item\n"; }
  0         0  
803 2         8 next ITEM;
804             }
805             # store item
806 7         19 $self->{channels}{$channel}{$typevar} = $item;
807             }
808             }
809 2         4 my $remainder = length($data);
810 2 50       8 if ($remainder > 0) { warn "Got a remainder when reading channel update."; }
  0         0  
811             }
812            
813             sub _send_ack {
814 0     0   0 my $self = shift;
815 0         0 my ($seq) = @_;
816            
817 0         0 my $rawdata = pack('H6C1H2C1', $START, $seq, $TYPES{ack}, 0);
818 0         0 my $checksum = $self->_checksum($rawdata);
819 0         0 my $data = $rawdata.$checksum;
820 0 0       0 if ($self->debug >= 3) {print '>> '.$self->_pformat($data)."\n"; }
  0         0  
821            
822 0         0 my $serial = $self->{_serial};
823 0         0 my $count_out = $serial->write($data);
824 0 0       0 warn "Not enough data written" unless ($count_out == length($data));
825             }
826            
827             sub _send_checksum_error {
828 0     0   0 my $self = shift;
829 0         0 my ($seq) = @_;
830            
831 0         0 my $rawdata = pack('H6C1H2C1', $START, $seq, $TYPES{e_checksum}, 0);
832 0         0 my $checksum = $self->_checksum($rawdata);
833 0         0 my $data = $rawdata.$checksum;
834 0 0       0 if ($self->debug >= 3) {print '>> '.$self->_pformat($data)."\n"; }
  0         0  
835            
836 0         0 my $serial = $self->{_serial};
837 0         0 my $count_out = $serial->write($data);
838 0 0       0 warn "Not enough data written" unless ($count_out == length($data));
839             }
840            
841             sub _send_command {
842             ### TODO: Handle escape char (1B)
843             # returns true/false results
844 0     0   0 my $self = shift;
845 0         0 my ($hexcommand) = @_;
846 0         0 my $command = pack('H*', $hexcommand);
847 0         0 my $cmdlength = length($command);
848 0         0 my $sequence = $self->{_sequence};
849            
850 0         0 my $rawdata = pack('H6C1H2C1a*', $START, $sequence, $TYPES{command}, $cmdlength, $command);
851 0         0 my $checksum = $self->_checksum($rawdata);
852             # oddly enough the double escapes don't count as length. don't change original length.
853 0         0 my $data = pack('H6C1H2C1a*a1', $START, $sequence, $TYPES{command}, $cmdlength, $command, $checksum);
854            
855             # handle the escape character anywhere in the sent data. must be done after checksum.
856 0         0 $data =~ s/\x1b/\x1b\x1b/g;
857            
858 0         0 my $serial = $self->{_serial};
859            
860 0         0 my $attempts=0;
861 0         0 SEND: foreach $attempts (1..5) {
862             # send/retry logic
863 0 0       0 if ($self->{debug}) { print "Sending command: $hexcommand sequence: $sequence\n"; }
  0         0  
864 0 0       0 if ($self->debug >= 3) {print '>> '.$self->_pformat($data)."\n"; }
  0         0  
865 0         0 $serial->write($data);
866 0         0 $self->_receive;
867 0 0       0 last SEND if ($self->{_lastack} == $sequence );
868             # we're still here... receiver is probably busy. give it a bit.
869 0         0 sleep(3);
870             }
871            
872 0         0 $self->{_sequence} = ($self->{_sequence} + 1);
873 0 0       0 if ($self->{_sequence} > 255) { $self->{_sequence} = 0; }
  0         0  
874            
875 0 0 0     0 if (($attempts == 3) && ($self->{lastack} != $sequence) ) {
876 0         0 carp "Command not acknowledged by tuner after 3 attempts.";
877 0         0 return 0;
878             }
879 0         0 return 1;
880             }
881            
882            
883             sub _checksum {
884             # returns 1 byte (char) of checksum data
885             # i can replace this with unpack. just need to do the 256-result thing.
886             # is there a bug here when $sum % 256 = 0?
887 4     4   5066 my $self = shift;
888 4         7 my ($data) = @_;
889            
890 4         4 my $char;
891 4         5 my $sum = 0;
892 4         33 foreach $char (split(//, $data)) {
893 161         189 $sum += ord($char);
894             }
895 4 100       28 if ( ($sum % 0x100) == 0) { return chr(0); }
  1         3  
896 3         5 my $cs = 0x100 - ($sum % 0x100);
897 3         10 return chr($cs);
898             }
899            
900             sub _pformat {
901 0     0     my $self = shift;
902 0           my ($data) = @_;
903 0           my $buffer = '';
904            
905 0           my $char;
906            
907 0           foreach $char (split(//, $data)) {
908 0           $char = ord($char);
909 0 0 0       if (($char >= 32) && ($char <= 126)) {
910             # $buffer .= chr($char);
911 0           $buffer .= sprintf ("0x%02x ", $char);
912             } else {
913 0           $buffer .= sprintf ("0x%02x ", $char);
914             }
915             }
916 0           return $buffer;
917             }
918            
919             sub _num_to_signed_hex {
920 0     0     my $self = shift;
921 0           my ($data) = @_;
922            
923 0           return (unpack('H2', pack ('c1', $data) ) );
924             }
925            
926             sub _num_to_unsigned_hex {
927 0     0     my $self = shift;
928 0           my ($data) = @_;
929            
930 0           return (unpack('H2', pack ('C1', $data) ) );
931             }
932            
933             =head1 DEPENDENCIES
934            
935             None yet.
936            
937             =head1 AUTHOR
938            
939             Jamie Tatum, L, C<< >>
940            
941             =head1 BUGS
942            
943             =over
944            
945             =item *
946            
947             You should be able to submit a function reference to be called when the various updates (channel info, time, signal, pid) occur. This is not yet
948             implemented.
949            
950             =item *
951            
952             The power system needs to be revisited. Currently C turns the radio off - it should probably preserve state between sessions.
953            
954             =item *
955            
956             The channel property isn't being set (correctly anyway).
957            
958             =item *
959            
960             Various public properties need to be documented.
961            
962             =back
963            
964             Please report any bugs or feature requests to
965             C, or through the web interface at
966             L.
967             I will be notified, and then you'll automatically be notified of progress on
968             your bug as I make changes.
969            
970             =head1 ACKNOWLEDGEMENTS
971            
972             Thanks to Mitch and Dale at L Thanks to everyone who reversed a little bit of the tuner protocol
973             - too many to list. :) You know who you are.
974            
975             =head1 COPYRIGHT & LICENSE
976            
977             Copyright 2005 Jamie Tatum, all rights reserved.
978            
979             Sirius and related marks are trademarks of SIRIUS Satellite Radio Inc. Use of this module is at your own risk and may be subject to the SIRIUS terms and
980             conditions located at L.
981            
982             This program is free software; you can redistribute it and/or modify it
983             under the same terms as Perl itself.
984            
985             =cut
986            
987             1; # End of Audio::Radio::Sirius