00001 #!/usr/bin/perl
00002
00003 # directv.pl: Remote control of a DirecTV unit via the serial port
00004 # By Josh Wilmes (http://www.hitchhiker.org/dss)
00005 # Based on info from http://www.isd.net/mevenmo/audiovideo.html
00006 #
00007 # I take no responsibility for any damage this script might cause.
00008 # Feel free to modify and redistribute this as you see fit, but please retain
00009 # the comments above.
00010
00011 # See usage command or run without and parameters for how to use
00012 # Documentation of box protocol and cables at
00013 # http://www.dtvcontrol.com/ and
00014 # http://www.knoppmythwiki.org/index.php?page=DtenSerialControlScript
00015
00016 # Modified by Dave Manaloto < dave@practicecode.com >
00017 # - Put discrete code to turn on IRD
00018 # - Try and clear OSD after changing channel
00019 # - sync computer to IRD time "--sync-time" (Now set_system_datetime)
00020
00021 # Modified by John Gruenenfelder < johng@as.arizona.edu >
00022 # - Use codes for newer RCA model receivers (same as Sony codes?)
00023 # Info from http://www.dar.net/~andy/tivo/rca_dss_serial.html
00024 # - Use Perl's select function to pause 0.20 seconds rather than external
00025 # usleep call
00026 # - Use 9600 baud rate instead of 115200
00027 #
00028 # Originally posted to KnoppMythWiki by Bret Shroyer <bret <at> bretshroyer ,dot, org> Dec 16, 2004
00029 #
00030 # Modified by David Gesswein < djg@pdp8.net > June 11, 2005
00031 # Added codes for Directv D10-200 receiver and D10-100 firmware 0x101B
00032 # Added many commands and tried to make flexible enough that users won't
00033 # have to edit this file
00034 # Added retry on errors since the D10-200 is not reliable
00035 #
00036 # Modified by Stacey Son <mythdev <at> son ,dot, org> Sept 9, 2005
00037 # Added codes for LG LSS-3200A/Sony SAT-HD300/Hughes HTL-HD receivers
00038 # Added get_info, enable_remote, and disable_remote (HD300 only) commands
00039 # Based on http://www.avsforum.com/avs-vb/showthread.php?p=3000174&&#post3000174
00040 # (Use a straight through serial cable like RadioShack 26-117B to connect)
00041
00042
00043 $|=1;
00044 use POSIX qw(:termios_h);
00045 use Time::HiRes qw(usleep ualarm gettimeofday tv_interval );
00046
00047 use FileHandle;
00048
00049 $version = "1.2";
00050
00051 #
00052 # Verbose output, change with verbose and quiet command.
00053 #
00054 $verbose=0;
00055
00056 #
00057 # Error Retries, change with retry command.
00058 #
00059 $retry_count=4;
00060
00061 #
00062 # Serial port settings. Change to suit. Baudrate probably doesn't need to be
00063 # changed. Set port with port command.
00064 #
00065 $baudrate = "9600";
00066 $serport = "/dev/ttyS0";
00067
00068 #
00069 # Box type, set with box_type command
00070 #
00071 $box_type = "RCA";
00072
00073 #
00074 # Delay to wait after channel change before turning off OSD in setup_channel
00075 # command. Can use separate commands with delay command to control delay.
00076 #
00077 $clear_osd_delay = .2;
00078
00079 %pkt_decode=("0xF0" => "START PKT",
00080 "0xF1" => "ERR 1",
00081 "0xF2" => "GOT EXTENDED",
00082 "0xF4" => "END PKT",
00083 "0xF5" => "ERR 2",
00084 "0xFB" => "PROMPT");
00085
00086 # -1 is command had error, 1 is command completed ok
00087 %terminal=("0xF1" => -1,
00088 "0xF4" => 1,
00089 "0xF5" => -1);
00090
00091 # Map commands to function to execute.
00092 # last_param is handled in main routine.
00093 # "show" => \&show, Doesn't seem to be in new command set
00094 # "scroll" => \&scroll,
00095 %cmds=("on" => \&on,
00096 "off" => \&off,
00097 "text" => \&text,
00098 "hide" => \&hide,
00099 "get_channel" => \&get_channel,
00100 "get_signal" => \&get_signal,
00101 "get_datetime" => \&get_datetime,
00102 "get_info" => \&get_info,
00103 "enable_remote" => \&enable_remote,
00104 "disable_remote" => \&disable_remote,
00105 "set_system_datetime" => \&set_system_datetime,
00106 "key" => \&key,
00107 "delay" => \&delay,
00108 "port" => \&port,
00109 "baudrate" => \&baudrate,
00110 "box_type" => \&box_type,
00111 "retries" => \&retries,
00112 "channel_change_type" => \&channel_change_type,
00113 "setup_channel" => \&setup_channel,
00114 "version" => \&version,
00115 "verbose" => \&set_verbose,
00116 "quiet" => \&clear_verbose
00117 );
00118
00119 # Key to keycode map for most boxes
00120 %keymap=(right => "0xa8",
00121 left => "0xa9",
00122 up => "0xa6",
00123 down => "0xa7",
00124 favorite => "0x9e",
00125 select => "0xc3",
00126 enter => "0xc3", # Doesn't have separate enter?
00127 exit => "0xc5",
00128 9 => "0xc6",
00129 8 => "0xc7",
00130 7 => "0xc8",
00131 6 => "0xc9",
00132 5 => "0xca",
00133 4 => "0xcb",
00134 3 => "0xcc",
00135 2 => "0xcd",
00136 1 => "0xce",
00137 0 => "0xcf",
00138 ch_up => "0xd2",
00139 ch_dn => "0xd3",
00140 power => "0xd5",
00141 jump => "0xd8",
00142 guide => "0xe5",
00143 menu => "0xf7");
00144
00145 # Key to keycode map for Directv D10-200 and D10-100 firmware 0x101B
00146 %keymap_200 =
00147 (right => "0x9a",
00148 left => "0x9b",
00149 up => "0x9c",
00150 down => "0x9d",
00151 select => "0xc3",
00152 enter => "0xa0",
00153 exit => "0xd4",
00154 9 => "0xe9",
00155 8 => "0xe8",
00156 7 => "0xe7",
00157 6 => "0xe6",
00158 5 => "0xe5",
00159 4 => "0xe4",
00160 3 => "0xe3",
00161 2 => "0xe2",
00162 1 => "0xe1",
00163 0 => "0xe0",
00164 dash => "0xa5",
00165 "-" => "0xa5",
00166 ch_up => "0xd1",
00167 ch_dn => "0xd2",
00168 power => "0xd5",
00169 jump => "0xd6", # Name from other set
00170 prev => "0xd6", # Key label on -200
00171 guide => "0xd3",
00172 menu => "0xf7",
00173 info => "0xa1",
00174 active => "0xa2",
00175 list => "0xa3",
00176 back => "0xa4"
00177 # Code B0 are accepted by box but I couldn't figure out what if
00178 # anything they do.
00179 );
00180
00181 # Key to keycode map for LG LSS-3200A/Sony SAT-HD300/Hughes HTL-HD
00182 %keymap_HD300 = (
00183 # it seems that the HD300 has a limited keymap
00184 right => "0x9a",
00185 left => "0x9b",
00186 up => "0x9c",
00187 down => "0x9d",
00188 select => "0xc3",
00189 enter => "0xc3", # enter/select/info
00190 exit => "0xc5", # power on
00191 power => "0xd5", # power toggle
00192 guide => "0xe5",
00193 menu => "0xf7"
00194 # Code 0xfa also brings up the menu
00195 );
00196
00197
00198 # From box name select correct key codes
00199 %boxes=("RCA" => \%keymap,
00200 "D10-100" => \%keymap_200,
00201 "D10-200" => \%keymap_200,
00202 "HD300" => \%keymap_HD300
00203 );
00204
00205 # From box name select extra bytes needed with key codes
00206 %keymap_extra=("RCA" => ["0x00", "0x00"],
00207 "D10-100" => ["0x00", "0x01"],
00208 "D10-200" => ["0x00", "0x01"],
00209 "HD300" => ["0x00", "0x00"]
00210 );
00211
00212 # From box name select extra bytes needed after sending command
00213 %cmd_extra=("RCA" => undef,
00214 "D10-100" => undef,
00215 "D10-200" => "0x0d",
00216 "HD300" => undef
00217 );
00218
00219 # From box name select if we should use the channel change command
00220 # or send remote keys. The D10-200 locks up randomly with the channel
00221 # change command and the D10-100 firmware 0x101B won't select below 100.
00222 %chan_change_key=("RCA" => 0,
00223 "D10-100" => 1,
00224 "D10-200" => 1,
00225 "HD300" => 0
00226 );
00227 # Override from command line for above
00228 my $chan_change_key_param;
00229
00230
00231 my $serial;
00232 my $i;
00233
00234 # Replace argument last_param with last parameter on the command line.
00235 # The last parameter is then removed.
00236 for ($i = 0; $i < $#ARGV; $i++) {
00237 if ($ARGV[$i] eq "last_param") {
00238 $ARGV[$i] = $ARGV[$#ARGV];
00239 $#ARGV = $#ARGV - 1;
00240 last;
00241 }
00242 }
00243
00244 if ($#ARGV < 0) {
00245 usage();
00246 }
00247
00248 while ($#ARGV >= 0) {
00249 if (defined($sub = $cmds{$ARGV[0]})) {
00250 shift @ARGV;
00251 &$sub;
00252 } else {
00253 if ($ARGV[0] == 0) {
00254 usage();
00255 die "\nCommand $ARGV[0] not found\n"
00256 }
00257 change_channel($ARGV[0]);
00258 shift @ARGV;
00259 }
00260 }
00261
00262 exit(0);
00263
00264 sub usage {
00265 print "Usage: $0 command ...\n";
00266 print "Commands:\n";
00267 print " box_type RCA|D10-100|D10-200|HD300 - select set top box type\n";
00268 print " delay number - wait for number seconds. Floating point is valid \n";
00269 print " key string - send remote key string. See source for supported keys\n";
00270 print " last_param - execute last parameter on command line at current location\n";
00271 print " number{-number} - change to specified channel-subchannel\n";
00272 print " off - turn box off\n";
00273 print " on - turn box on\n";
00274 print " port string - select port to send commands on, currently $serport\n";
00275 print " setup_channel number - send on, channel change command and OSD off command\n";
00276 print " version - display program version\n";
00277 print "\n";
00278 print " baudrate number - select serial port baudrate, currently $baudrate\n";
00279 print " channel_change_type key|command - select channel change method\n";
00280 print " get_channel - print current channel\n";
00281 print " get_datetime - print date and time\n";
00282 print " get_signal - print signal strength\n";
00283 print " get_info - print information (HD300 only?)\n";
00284 print " enable_remote - enable remote control (HD300 only?)\n";
00285 print " disable_remote - disable remote control (HD300 only?)\n";
00286 print " hide - hide text, will also prevent info button from working\n";
00287 print " retries number - set maximum number of retries on error\n";
00288 print " set_system_datetime - set PC clock from box. ntp is more accurate\n";
00289 print " text string - display string on screen, \"\" to clear\n";
00290 print " verbose|quiet - select how much information printed\n";
00291 print "\n";
00292 print "Mythtv command for normal RCA box: directv.pl setup_channel\n";
00293 print "Complex Mythtv command for D10-200 box doing same as setup_channel:\n";
00294 print " directv.pl box_type D10-200 on last_param delay .2 key exit\n";
00295 print "Mythtv adds channel number at end of command\n";
00296 }
00297
00298 sub setup_channel {
00299 on();
00300 change_channel(shift(@ARGV));
00301 select(undef, undef, undef, $clear_osd_delay);
00302 if ($box_type eq "HD300") {
00303 # "exit" key doesn't seem clear the OSD on HD300
00304 send_key("enter");
00305 } else {
00306 send_key("exit");
00307 }
00308 }
00309
00310 sub version {
00311 print "Version $version\n";
00312 }
00313
00314 sub retries {
00315 $retry_count = shift(@ARGV);
00316 }
00317
00318 sub channel_change_type {
00319 my $type = shift(@ARGV);
00320 if ($type eq "key") {
00321 $chan_change_key_param = 1;
00322 } elsif ($type eq "command") {
00323 $chan_change_key_param = 0;
00324 } else {
00325 die "Unkown channel_change_type $type\n";
00326 }
00327 }
00328
00329 sub key {
00330 send_key(shift(@ARGV));
00331 }
00332
00333 sub send_key {
00334 my $map = $boxes{$box_type};
00335 my $key = $$map{shift(@_)};
00336 die "Unknown key $ARGV[0]\n" if (!defined($key));
00337 simple_command("0xA5",@{$keymap_extra{$box_type}}, "0x$key");
00338 }
00339
00340 sub box_type {
00341 my ($tmp) = uc(shift(@ARGV));
00342 die "Unknown box_type $tmp\n" if (!defined($boxes{$tmp}));
00343 $box_type = $tmp;
00344 }
00345
00346 sub port {
00347 $serport = $ARGV[0];
00348 shift @ARGV;
00349 }
00350
00351 sub baudrate {
00352 $baudrate = $ARGV[0];
00353 shift @ARGV;
00354 }
00355
00356 sub delay {
00357 select(undef, undef, undef, $ARGV[0]);
00358 shift @ARGV;
00359 }
00360
00361 sub set_verbose {
00362 $verbose = 1;
00363 }
00364
00365 sub clear_verbose {
00366 $verbose = 0;
00367 }
00368
00369 sub on {
00370 if ($box_type eq "HD300") {
00371 return(command_response("0x82"));
00372 } else {
00373 return(simple_command("0x82"));
00374 }
00375 }
00376
00377 sub off {
00378 if ($box_type eq "HD300") {
00379 return(command_response("0x81"));
00380 } else {
00381 return(simple_command("0x81"));
00382 }
00383 }
00384
00385 sub get_channel {
00386 my @in = dss_command(4, "0x87");
00387 if ($box_type eq "HD300") {
00388 return if($#in != 4);
00389 } else {
00390 return if($#in != 3);
00391 }
00392 my $sub = $in[2] * 256 + $in[3];
00393 print "channel " , $in[0]*256+$in[1];
00394 print "-$sub" if $sub != 65535;
00395 print "\n";
00396 }
00397
00398 sub get_signal {
00399 my @in = dss_command(1, "0x90");
00400 return if($#in != 0);
00401 print "signal $in[0]\n";
00402
00403 }
00404
00405 sub get_datetime {
00406 my @in = dss_command(7, "0x91");
00407 if ($box_type eq "HD300") {
00408 return if($#in != 8);
00409 } else {
00410 return if($#in != 6);
00411 }
00412 $strTime = "$in[1]/$in[2] $in[3]:$in[4]:$in[5]";
00413 print("Date $strTime\n")# if ($verbose);
00414 }
00415
00416 sub set_system_datetime {
00417 my @in = dss_command(7, "0x91");
00418 return if($#in != 6);
00419 my $strTime = "$in[1]/$in[2] $in[3]:$in[4]:$in[5]";
00420 print("Setting system time to $strTime\n") if ($verbose);
00421 $cmd = "echo date -s \"$strTime\"";
00422 `$cmd`;
00423 }
00424
00425 sub enable_remote {
00426 return(command_response("0x93")) if ($box_type eq "HD300");
00427 }
00428
00429 sub disable_remote {
00430 return(command_response("0x94")) if ($box_type eq "HD300");
00431 }
00432
00433 sub get_info {
00434 my @in = dss_command(46, "0x83");
00435 return if($#in < 40);
00436
00437 # The HD300 info packet looks like the following:
00438 # [ 0 - 3] Channel Number
00439 # [ 4 - 15] Unknown
00440 # [16 - 24] Date/Time
00441 # [25 - 37] Unknown
00442 # [ 38 ] Signal Strength
00443 # [39 - 44] Unknown
00444
00445 # channel number... (offset 0 Len 4)
00446 my $sub = $in[2] * 256 + $in[3];
00447 print "channel " , $in[0]*256+$in[1];
00448 print "-$sub" if $sub != 65535;
00449 print "\n";
00450
00451 # Date/Time... (offset 16 Len 8)
00452 $strTime = "$in[16 + 1]/$in[16 + 2] $in[16 + 3]:$in[16 + 4]:$in[16 + 5]";
00453 print("Date $strTime\n");
00454
00455 # Signal... (offset 38 len 1)
00456 print "signal $in[38 + 0]\n";
00457 }
00458
00459 sub text {
00460 my @tmp = unpack("H2" x length($ARGV[0]) ,$ARGV[0]);
00461 shift @ARGV;
00462 simple_command("0xaa", sprintf("%x",$#tmp+1), @tmp);
00463 }
00464
00465 sub hide {
00466 if ($box_type eq "HD300") {
00467 # for some reason the "hide" command doesn't seem to work
00468 # the following is a hack that works
00469 send_key("enter");
00470 send_key("enter");
00471 } else {
00472 return(simple_command("0x86"));
00473 }
00474 }
00475
00476 sub simple_command {
00477 if (defined(dss_command(0, @_))) {
00478 return(1);
00479 } else {
00480 return(undef);
00481 }
00482 }
00483
00484 sub command_response {
00485 my @rc = dss_command(1, @_);
00486 if (defined(@rc)) {
00487 if ($rc[0] != 0) {
00488 return(0);
00489 } else {
00490 return(1);
00491 }
00492 } else {
00493 return(undef);
00494 }
00495 }
00496
00497 sub dss_command {
00498 my $reply_size = shift(@_);
00499 my $command_code = shift(@_);
00500 my $rc;
00501
00502 for (my $i = 0; $i < $retry_count; $i++) {
00503 if ($box_type eq "HD300" && $reply_size == 0) {
00504 sendbytes("0xFA", $command_code);
00505 $rc = get_reply($reply_size);
00506 next if (!defined($rc));
00507 sendbytes(@_);
00508 } else {
00509 sendbytes("0xFA", $command_code, @_);
00510 }
00511 $rc = get_reply($reply_size);
00512 if (defined($rc)) {
00513 return @{$rc};
00514 }
00515 # Clear any extra junk received on error
00516 sysread($serial,$buf,100);
00517 #print STDERR "Retry " , scalar localtime(time()) , "\n";
00518 }
00519 die "Error excessive retries\n";
00520 }
00521
00522 # Send channel change command or remote key pushes to change channel
00523 sub change_channel {
00524 my $change_key;
00525 if (defined($chan_change_key_param)) {
00526 $change_key = $chan_change_key_param;
00527 } else {
00528 $change_key = $chan_change_key{$box_type};
00529 }
00530 if ($change_key) {
00531 foreach $ch (split
00532 send_key($ch);
00533 }
00534 send_key("enter");
00535 } else {
00536
00537 my ($channel,$sub)= split /-/,@_[0];
00538 my $s1,$s2,$n1,$n2;
00539 $_=sprintf("%4.4x",$channel);
00540 ($n1,$n2)=/(..)(..)/;
00541 if (defined($sub)) {
00542 $_=sprintf("%4.4x",$sub);
00543 ($s1,$s2)=/(..)(..)/;
00544 } else {
00545 $s1 = "0xff";
00546 $s2 = "0xff";
00547 }
00548 simple_command("0xA6",$n1,$n2,$s1,$s2);
00549 }
00550 }
00551
00552
00553 sub sendbytes {
00554 my (@send)=@_;
00555 my $fullstr = "";
00556 if (!$serial) {
00557 $serial=init_serial($serport,$baudrate);
00558 }
00559 if (defined($cmd_extra{$box_type})) {
00560 push @send,$cmd_extra{$box_type};
00561 }
00562 foreach (@send) { s/^0x
00563 print "SEND: " if ($verbose);
00564 foreach $num (@send) {
00565 $str = pack('C',$num);
00566 $fullstr = $fullstr . $str;
00567 printf("0x%X [%s] ", $num, $str) if ($verbose);
00568 }
00569 syswrite($serial,$fullstr,length($fullstr));
00570 print "\n" if ($verbose);
00571 }
00572
00573
00574 # Get reply back. Reply should start with F0 then reply size bytes which
00575 # are passed back without examination then look for error or ok status.
00576 # Pass back any other bytes not part of status also.
00577 sub get_reply() {
00578 my ($reply_size) = @_;
00579 #my $starttime=time();
00580 my $starttime=[gettimeofday];
00581 my $found_start = 0;
00582 my ($last,$ok,@ret,$rc);
00583 @ret=();
00584
00585 print "RECV: " if ($verbose);
00586
00587 while (1) {
00588 $rc=sysread($serial,$buf,1);
00589 if ($rc < 0) {
00590 print STDERR "Read Error ($rc)\n" if ($verbose);
00591 last;
00592 }
00593 if ($rc == 0) {
00594 if (tv_interval($starttime) > .8) {
00595 #if (time() - ($starttime) > 2) {
00596 print STDERR "Timeout Error\n" if ($verbose);
00597 last;
00598 }
00599 next;
00600 }
00601
00602 $str=sprintf("0x%2.2X", ord($buf));
00603
00604 if ((!$found_start || $reply_size <= 0) && $pkt_decode{$str}) {
00605 print $str if ($verbose);
00606 print "[$pkt_decode{$str}] " if ($verbose);
00607 } else {
00608 $_=$str; s/^0x
00609 printf("$str(%3.3s) ",$_) if ($verbose);
00610 push (@ret,$_);
00611 }
00612
00613 if ($box_type eq "HD300" && $reply_size == 0) {
00614 # HD300 does a simple ack (F0 only) to say it's ready for more
00615 $ok=1 if ($str eq "0xF0");
00616 last if ($str eq "0xF0");
00617 }
00618
00619 if ($found_start && $reply_size-- <= 0) {
00620 $ok=1 if ($terminal{$str} > 0);
00621 last if ($terminal{$str});
00622 last if ($last eq "0xFB" && $str eq "0xFB");
00623 }
00624 $found_start = 1 if ($str eq "0xF0");
00625 $found_start = 1 if ($str eq "0xF2" && $box_type eq "HD300");
00626 $last=$str;
00627 }
00628 print "\n\n" if ($verbose);
00629
00630 return \@ret if ($ok);
00631 return undef;
00632 }
00633
00634
00635 sub init_serial {
00636 my($port,$baud)=@_;
00637 my($termios,$cflag,$lflag,$iflag,$oflag);
00638 my($voice);
00639
00640 my $serial=new FileHandle("+>$port") || die "Could not open $port: $!\n";
00641
00642 $termios = POSIX::Termios->new();
00643 $termios->getattr($serial->fileno()) || die "getattr: $!\n";
00644 $cflag= 0 | CS8 | HUPCL | CREAD | CLOCAL;
00645 $lflag= 0;
00646 $iflag= 0 | IGNBRK | IGNPAR;
00647 $oflag= 0;
00648
00649 $termios->setcflag($cflag);
00650 $termios->setlflag($lflag);
00651 $termios->setiflag($iflag);
00652 $termios->setoflag($oflag);
00653 $termios->setattr($serial->fileno(),TCSANOW) || die "setattr: $!\n";
00654 eval qq[
00655 \$termios->setospeed(POSIX::B$baud) || die "setospeed: \$!\n";
00656 \$termios->setispeed(POSIX::B$baud) || die "setispeed: \$!\n";
00657 ];
00658
00659 die $@ if $@;
00660
00661 $termios->setattr($serial->fileno(),TCSANOW) || die "setattr: $!\n";
00662
00663 # Make reads wait up to 200ms for a character
00664 $termios->getattr($serial->fileno()) || die "getattr: $!\n";
00665 $termios->setcc(VMIN,0);
00666 $termios->setcc(VTIME,2);
00667 $termios->setattr($serial->fileno(),TCSANOW) || die "setattr: $!\n";
00668
00669 return $serial;
00670 }