File Coverage

blib/lib/Device/Denon/DN1400F.pm
Criterion Covered Total %
statement 24 238 10.0
branch 0 88 0.0
condition 0 2 0.0
subroutine 8 39 20.5
pod 0 26 0.0
total 32 393 8.1


line stmt bran cond sub pod time code
1             package Device::Denon::DN1400F;
2              
3 1     1   6490 use 5.006;
  1         3  
  1         53  
4 1     1   5 use strict;
  1         2  
  1         37  
5 1     1   6 use vars qw(@ISA $VERSION @EXPORT_OK %EXPORT_TAGS %COMMANDS);
  1         14  
  1         77  
6 1     1   6 use warnings;
  1         2  
  1         34  
7 1     1   4 use Exporter;
  1         1  
  1         31  
8 1     1   944 use Data::Dumper;
  1         10197  
  1         77  
9 1     1   1065 use Device::SerialPort qw(:PARAM :STAT);
  1         36784  
  1         306  
10 1     1   1026 use Time::HiRes qw(usleep time);
  1         1790  
  1         4  
11              
12             @ISA = qw(Exporter);
13             @EXPORT_OK = qw();
14             %EXPORT_TAGS = ('all' => \@EXPORT_OK);
15             $VERSION = '0.02';
16              
17             %COMMANDS = (
18             MOVE_FRONT => {
19             Command => [ qw(ID 0xb2) ],
20             Answer => [ qw(0x80 ID) ],
21             Busy => [ qw(INVD ID) ],
22             },
23             CLEAR_CHANGER_BUFFER => {
24             Command => [ qw(ID 0xC0) ],
25             Answer => [ qw(0x80 ID) ],
26             Busy => [ qw(INVD ID) ],
27             },
28             _1BYTE_ERROR_CODE => {
29             Command => [ qw(ID 0xC1) ],
30             Answer => [ qw(ERR0 ID) ],
31             Busy => [ qw(INVD ID) ],
32             },
33             _2BYTE_ERROR_CODE => {
34             Command => [ qw(ID 0xC2) ],
35             Answer => [ qw(ERR0 ERR1 ID) ],
36             Busy => [ qw(INVD ID) ],
37             },
38             DISC_NUMBER => {
39             Command => [ qw(ID 0xC3) ],
40             Answer => [ qw(DNO_F DNO_R ID) ],
41             Busy => [ qw(INVD ID) ],
42             },
43             SELECT_A_DISC => {
44             Command => [ qw(ID 0xC4 DSCP DSCN DID) ],
45             Answer => [ qw(CST0 ID DID) ],
46             Busy => [ qw(INVD ID DID) ],
47             },
48             RETURN_A_DISC => {
49             Command => [ qw(ID 0xC5 DSCP DSCN DID) ],
50             Answer => [ qw(CST0 ID DID) ],
51             Busy => [ qw(INVD ID DID) ],
52             },
53             RETURN_ALL_DISC => {
54             Command => [ qw(ID 0xC6) ],
55             Answer => [ qw(CST0 ID) ],
56             Busy => [ qw(INVD ID) ],
57             },
58             RESET_DN_1400F => {
59             Command => [ qw(ID 0xCA) ],
60             Answer => [ qw() ],
61             Busy => [ qw(INVD ID) ],
62             },
63             CHANGER_MICON_VERSION => {
64             Command => [ qw(ID 0xCB 0x00) ],
65             Answer => [ qw(VER0 VER1 ID) ],
66             Busy => [ qw(INVD ID) ],
67             },
68             DISCNUMBER_CHANGER_STATUS=>{
69             Command => [ qw(ID 0xCC) ],
70             Answer => [ qw(DSCP_0 DSCN_0 CST0_0
71             DSCP_1 DSCN_1 CST0_1 CST1 ID)],
72             Busy => [ qw(INVD ID) ],
73             },
74             DRIVE_STATUS => {
75             Command => [ qw(ID 0xCB DID) ],
76             Answer => [ qw(DST0 ID DID) ],
77             Busy => [ qw(INVD ID DID) ],
78             },
79             DRIVE_MICON_VERSION => {
80             Command => [ qw(ID 0xD1 DID) ],
81             Answer => [ qw(VER0 VER1 ID DID) ],
82             Busy => [ qw(INVD ID DID) ],
83             },
84             DRIVE_STATUS_SERVOONOFF => {
85             Command => [ qw(ID 0xD2 DID) ],
86             Answer => [ qw(DST0 DST1 ID DID) ],
87             Busy => [ qw(INVD ID DID) ],
88             },
89             SUB_CODE_QMODE3 => {
90             Command => [ qw(ID 0xD6 DID) ],
91             Answer => [ qw(DST0 CTR_L),
92             map{"ISRC_$_"}(0..7),
93             qw(AFR_M ID DID)],
94             Busy => [ qw(INVD ID DID) ],
95             },
96             SUB_CODE_QMODE2 => {
97             Command => [ qw(ID 0xD7 DID) ],
98             Answer => [ qw(DST0 CTR_L),
99             map{"UPC_$_"}(0..7),
100             qw(AFR_M ID DID)],
101             Busy => [ qw(INVD ID DID) ],
102             },
103             SUB_CODE_QCHANNEL => {
104             Command => [ qw(ID 0xD9 DID) ],
105             Answer => [ qw(DST0 CTRL TNO INX MIN SEC FRM 0x00
106             AMIN ASEC AFRM ID DID) ],
107             Busy => [ qw(INVD ID DID) ],
108             },
109             ALL_TOC_DATA => {
110             Command => [ qw(ID 0xDA DID) ],
111             Answer => [ qw(0xA0 PMIN 0x00 0x00 CTRL
112             YADDA EOT 0x00
113             YADDA DST0 ID DID) ],
114             Busy => [ qw(INVD ID DID) ],
115             Variable=> 1,
116             },
117             SHORT_TOC_DATA => {
118             Command => [ qw(ID 0xDB DID) ],
119             Answer => [ qw(0xA0 PMIN 0x00 0x00
120             CTRL YADDA EOT DST0 ID DID) ],
121             Busy => [ qw(INVD ID DID) ],
122             Variable=> 1,
123             },
124             PLAY_AUDIO => {
125             Command => [ qw(ID 0xE2 AMIN ASEC AFRM TNO INX MODE DID) ],
126             Answer => [ qw(DST0 ID DID) ],
127             Busy => [ qw(INVD ID DID) ],
128             },
129             AUDIO_SCAN => {
130             Command => [ qw(ID 0xE3 AMIN ASEC AFRM TNO INX MODE DID) ],
131             Answer => [ qw(DST0 ID DID) ],
132             Busy => [ qw(INVD ID DID) ],
133             },
134             PAUSE => {
135             Command => [ qw(ID 0xE5 MODE DID) ], # Docs are buggy
136             Answer => [ qw(DST0 ID DID) ],
137             Busy => [ qw(INVD ID DID) ],
138             },
139             SEEK => {
140             Command => [ qw(ID 0xE6 AMIN ASEC AFRM DID) ],
141             Answer => [ qw(DST0 ID DID) ],
142             Busy => [ qw(INVD ID DID) ],
143             },
144             STOP => {
145             Command => [ qw(ID 0xE7 DID) ],
146             Answer => [ qw(DST0 ID DID) ],
147             Busy => [ qw(INVD ID DID) ],
148             },
149             # 25-26 reserved
150             TRACK_SEARCH => {
151             Command => [ qw(ID 0xEC AMIN ASEC AFRM TNO INX MODE DID) ],
152             Answer => [ qw(DST0 ID DID) ],
153             Busy => [ qw(INVD ID DID) ],
154             },
155             AUDIO_CHANNEL_CONTROL => {
156             Command => [ qw(ID 0xED) ],
157             Answer => [ qw(CST0 ID) ],
158             Busy => [ qw(INVD ID DID) ],
159             },
160             # 29 reserved
161             FADE_INOUT_PLAY => {
162             Command => [ qw(ID 0xF2 AMIN ASEC AFRM TNO INX MODE DID) ],
163             Answer => [ qw(DST0 ID DID) ],
164             Busy => [ qw(INVD ID DID) ],
165             },
166             SYSTEM_MICON_VERSION => {
167             Command => [ qw(ID 0xF3) ],
168             Answer => [ qw(VER0 VER1 ID) ],
169             Busy => [ qw(INVD ID DID) ],
170             },
171             );
172              
173              
174             sub new {
175 0     0 0   my $class = shift;
176 0 0         my $self = ($#_ == 0) ? { %{ (shift) } } : { @_ };
  0            
177              
178 0 0         die "No SerialPort specified" unless $self->{SerialPort};
179              
180             # Device::SerialPort->debug(1);
181              
182 0           my $port = new Device::SerialPort(
183             $self->{SerialPort},
184             0,
185             undef);
186 0 0         die "Failed to open device $self->{SerialPort}" unless $port;
187              
188 0           $port->user_msg(1);
189 0           $port->error_msg(1);
190             # $port->debug(1);
191              
192 0           $port->baudrate(19200);
193 0           $port->parity("even");
194 0           $port->parity_enable("yes");
195 0           $port->databits(8);
196 0           $port->stopbits(1);
197 0           $port->handshake("none");
198              
199 0 0         $port->write_settings or die "Failed to write settings\n";
200              
201 0           $port->status;
202              
203 0           $self->{Port} = $port;
204              
205 0           $self->{LastCommand} = time;
206              
207 0           return bless $self, $class;
208             }
209              
210             sub commands {
211 0     0 0   return keys(%COMMANDS);
212             }
213              
214             sub _cmd {
215 0     0     my $self = shift;
216 0           my $command = shift;
217 0 0         my $args = ($#_ == 0) ? { %{ (shift) } } : { @_ };
  0            
218              
219             # We can't send the commands to it too fast. It confuses it.
220 0           while (time - $self->{LastCommand} < 0.2) {
221 0           usleep(50);
222             }
223 0           $self->{LastCommand} = time;
224              
225 0           my $data = $COMMANDS{$command};
226 0 0         die "No such command $command" unless $data;
227              
228 0           print "Executing command $command\n";
229              
230 0           my @template = @{ $data->{Command} };
  0            
231 0           my @bytes = ();
232 0           foreach (@template) {
233 0 0         if ($_ =~ /^0x[[:xdigit:]]+$/) {
    0          
234 0           push(@bytes, hex($_));
235             }
236             elsif ($_ eq 'ID') {
237 0           push(@bytes, $self->{Id} + 0x50);
238             }
239             else {
240 0 0         die "No value for required parameter $_"
241             unless exists $args->{$_};
242 0           print "$_ = $args->{$_}\n";
243 0           push(@bytes, $args->{$_});
244             }
245             }
246              
247 0           my @hex = map { sprintf("%2.2x", $_) } @bytes;
  0            
248 0           my $string = pack("C*", @bytes);
249 0           my $count = $self->{Port}->write($string);
250 0           print "<< @hex\n";
251 0 0         die "Wrote only $count bytes" unless $count == length $string;
252              
253 0 0         return { } if $command eq 'RESET_DN_1400F';
254              
255 0           my $prefix;
256 0           my $timer = 0;
257 0           while (1) {
258 0           usleep(10);
259 0           ($count, $prefix) = $self->{Port}->read(1);
260 0 0         last if $count;
261 0 0         die "Got no response to command!" if ++$timer > 100;
262             }
263              
264 0 0         if ($prefix eq "\xdd") {
265 0           print "Response is an error code.\n";
266 0           @template = @{ $data->{Busy} };
  0            
267             }
268             else {
269 0           @template = @{ $data->{Answer} };
  0            
270             }
271              
272 0           my $readlength;
273 0 0         if ($data->{Variable}) {
274             # Wait long enough for the data to come down the line.
275 0           usleep(400000);
276             # Long enough for ALL_TOC_DATA. In theory we should read
277             # and lex, rather than trying to slurp, then we would know
278             # when the end of the data is.
279 0           $readlength = 1000;
280             }
281             else {
282 0           $readlength = scalar(@template) - 1;
283             }
284              
285 0           ($count, $string) = $self->{Port}->read($readlength);
286 0           my @response = unpack("C*", $prefix . $string);
287              
288 0           @hex = map { sprintf("%2.2x", $_) } @response;
  0            
289 0           print ">> @hex\n";
290              
291             # A slight kludge to get this out of the system.
292 0 0         return { Data => \@response } if $data->{Variable};
293              
294 0 0         die "Reponse template not same size as response"
295             unless @template == @response;
296              
297 0           my %out = map { $template[$_] => $response[$_] } (0..$#template);
  0            
298 0           return \%out;
299             }
300              
301             sub command {
302 0     0 0   my $self = shift;
303 0           my $response = $self->_cmd(@_);
304 0           $self->print_response($response);
305 0           return $response;
306             }
307              
308             my %ID = map { $_ + 0x50 => "Unit $_" } (0..15);
309              
310             my %CST0 = (
311             0x80 => "Command complete, reception normally completed.",
312             0x81 => "No Disc",
313             0x82 => "Busy, Disc transport section is in disc transport processing",
314             0x83 => "Completed Disc Set with No Error",
315             0x84 => "Reserved",
316             0x85 => "Reserved",
317             0x86 => "Reserved",
318             0x8A => "Initial Busy, After power on and Reset DN-1400F",
319             0x8B => "Changer Error",
320             0x8C => "Disc Rack in not set",
321             0x8E => "Wait transportation",
322             0x8F => "Changer Error",
323             0xDD => "INVD, Command Busy or Invalid Command",
324             );
325              
326             my %DST0 = (
327             0xB0 => "Ready, Reception normally completed.",
328             0xB1 => "Fade In / Out Play, In the process of fade in/out play",
329             0xB2 => "Seek, In the process of search.",
330             0xB3 => "Reserved",
331             0xB4 => "Pause, Pause condition during audio play.",
332             0xB5 => "Scan, In the process of scan play execution.",
333             0xB6 => "Play, In the process of audio play.",
334             0xB7 => "Reserved",
335             0xB8 => "Disc Change. Disc has been changed.",
336             0xB9 => "No Disc, Disc is not set in the disc loading section.",
337             0xBA => "Reserved",
338             0xBB => "Seek Error",
339             0xBC => "EOT: End of TOC",
340             0xBF => "CD-ROM Data Area",
341             0xD0 => "RAM Error (CD-DRIVE Hardware Error)",
342             0xD1 => "FOK Error (CD-DRIVE Hardware Error)",
343             0xD2 => "FZC Error (CD-DRIVE Hardware Error)",
344             0xD3 => "GFS Error (CD-DRIVE Hardware Error)",
345             0xD5 => "Slide Error (CD-DRIVE Hardware Error)",
346             0xD6 => "Eject Sequence Error (CD-DRIVE Hardware Error)",
347             0xD7 => "Gain Control Error (CD-DRIVE Hardware Error)",
348             0xD8 => "Reserved",
349             0xD9 => "Reserved",
350             0xDA => "Reserved",
351             0xDB => "Invalid Command or Invalid Parameter",
352             0xDC => "Invalid Parameter",
353             0xDD => "INVD: Command busy or Invalid Command.",
354             );
355              
356             my %DST1 = (
357             0x00 => "Servo off",
358             0x01 => "Servo on",
359             );
360              
361             my %DID = (
362             0x00 => "Drive 1: Front",
363             0x01 => "Drive 2: Rear",
364             );
365              
366             my %ERR = (
367             0x00 => "No error",
368             );
369              
370             sub print_response_item {
371 0     0 0   my ($self, $response, $key, $values) = @_;
372 0 0         if (exists $response->{$key}) {
373 0 0 0       my $value = $values
374             ? ($values->{$response->{$key}} || "VALUE UNKNOWN!")
375             : $response->{$key};
376 0           print "* $key: " .
377             sprintf("%x", $response->{$key}) . " : $value\n";
378 0           delete $response->{$key};
379             }
380             }
381              
382             sub print_response {
383 0     0 0   my ($self, $response) = @_;
384              
385 0           my %copy = %$response;
386 0           $self->print_response_item(\%copy, "ID", \%ID);
387 0           $self->print_response_item(\%copy, "DID", \%DID);
388 0           $self->print_response_item(\%copy, "CST0", \%CST0);
389 0           $self->print_response_item(\%copy, "DST0", \%DST0);
390 0           $self->print_response_item(\%copy, "DST1", \%DST1);
391 0           $self->print_response_item(\%copy, "DID", \%DID);
392 0           $self->print_response_item(\%copy, "ERR0", \%ERR);
393 0           $self->print_response_item(\%copy, "ERR1", \%ERR);
394 0           foreach (keys %copy) {
395 0 0         if ($_ =~ /^0x/) {
396 0 0         if (hex($_) != $response->{$_}) {
397 0           print "Expected $_, got " .
398             sprintf("%2.2x\n", $response->{$_});
399             }
400             }
401             else {
402 0           $self->print_response_item(\%copy, $_, undef);
403             }
404             }
405             }
406              
407             sub _dscpn {
408 0     0     my $discno = shift;
409              
410 0           my ($dscp, $dscn);
411              
412 0 0         if ($discno < 0) {
    0          
    0          
    0          
    0          
413 0           die "Invalid disc number $discno\n";
414             }
415             elsif ($discno <= 50) {
416 0           $dscp = 0;
417 0           $dscn = $discno - 1;
418             }
419             elsif ($discno <= 100) {
420 0           $dscp = 1;
421 0           $dscn = $discno - 51;
422             }
423             elsif ($discno <= 150) {
424 0           $dscp = 2;
425 0           $dscn = $discno - 101;
426             }
427             elsif ($discno <= 200) {
428 0           $dscp = 3;
429 0           $dscn = $discno - 151;
430             }
431             else {
432 0           die "Invalid disc number $discno\n";
433             }
434              
435 0           return ($dscp, $dscn);
436             }
437              
438             sub _discno {
439 0     0     my ($dscp, $dscn) = @_;
440 0 0         return -1 if $dscp == 255;
441 0           return $dscp * 50 + $dscn + 1;
442             }
443              
444             sub _from_bcd {
445 0     0     my $val = shift;
446 0           return 0+ sprintf("%x", $val);
447             }
448              
449             # Interpreting a number as hex essentially codes it as BCD.
450             sub _to_bcd {
451 0     0     my $val = shift;
452 0           return hex($val);
453             }
454              
455             sub move_front {
456 0     0 0   my ($self) = @_;
457 0           return $self->command('MOVE_FRONT');
458             }
459              
460             sub clear_changer_buffer {
461 0     0 0   my ($self) = @_;
462 0           return $self->command('CLEAR_CHANGER_BUFFER');
463             }
464              
465             sub debug {
466 0     0 0   my ($self) = @_;
467 0           $self->command('_1BYTE_ERROR_CODE');
468 0           $self->command('_2BYTE_ERROR_CODE');
469             }
470              
471             sub loaded_discs {
472 0     0 0   my ($self) = @_;
473 0           my $response = $self->command('DISC_NUMBER');
474 0           return ($response->{DNO_F}, $response->{DNO_R});
475             }
476              
477             sub load_disc {
478 0     0 0   my ($self, $drive, $discno, $args) = @_;
479 0 0         $args = {} unless ref($args) eq 'HASH';
480 0           $args->{DID} = $drive;
481 0           ($args->{DSCP}, $args->{DSCN}) = _dscpn($discno);
482 0           return $self->command('SELECT_A_DISC', $args);
483             }
484              
485             sub unload_disc {
486 0     0 0   my ($self, $drive, $discno, $args) = @_;
487 0 0         $args = {} unless ref($args) eq 'HASH';
488 0           $args->{DID} = $drive;
489 0           ($args->{DSCP}, $args->{DSCN}) = _dscpn($discno);
490 0           return $self->command('RETURN_A_DISC', $args);
491             }
492              
493             sub unload_discs {
494 0     0 0   my ($self) = @_;
495 0           return $self->command('RETURN_ALL_DISC');
496             }
497              
498             sub reset {
499 0     0 0   my ($self) = @_;
500 0           return $self->command('RESET_DN_1400F');
501             }
502              
503             # Calling this immediately after loaded_discs barfs. Firmware bug?
504             sub changer_version {
505 0     0 0   my ($self) = @_;
506             # This command seems to be broken on mine.
507 0           my $response = $self->command('CHANGER_MICON_VERSION');
508 0           return ($response->{VER0}, $response->{VER1})
509             }
510              
511             sub status {
512 0     0 0   my ($self) = @_;
513             # This command seems to be broken on mine.
514 0           my $response = $self->command('DISCNUMBER_CHANGER_STATUS');
515             return {
516 0           Disc0 => _discno($response->{DSCP_0},
517             $response->{DSCN_0}),
518             Disc1 => _discno($response->{DSCP_1},
519             $response->{DSCN_1}),
520             Status0 => $CST0{$response->{CST0_0}},
521             Status1 => $CST0{$response->{CST0_1}},
522             };
523             }
524              
525             # As far as I can work out, the firmware on this one is buggy too.
526             sub drive_status {
527 0     0 0   my ($self, $drive, $args) = @_;
528 0           die "Buggy firmware in the drive_status command.";
529 0 0         $args = {} unless ref($args) eq 'HASH';
530 0           $args->{DID} = $drive;
531 0           my $response = $self->command('DRIVE_STATUS', $args);
532             }
533              
534             sub drive_version {
535 0     0 0   my ($self, $drive, $args) = @_;
536 0 0         $args = {} unless ref($args) eq 'HASH';
537 0           $args->{DID} = $drive;
538 0           my $response = $self->command('DRIVE_MICON_VERSION', $args);
539             }
540              
541             sub drive_status_servo_onoff {
542 0     0 0   my ($self, $drive, $args) = @_;
543 0 0         $args = {} unless ref($args) eq 'HASH';
544 0           $args->{DID} = $drive;
545 0           my $response = $self->command('DRIVE_STATUS_SERVOONOFF', $args);
546             }
547              
548             sub drive_subcode_qchannel {
549 0     0 0   my ($self, $drive, $args) = @_;
550 0 0         $args = {} unless ref($args) eq 'HASH';
551 0           $args->{DID} = $drive;
552 0           my $response = $self->command('SUB_CODE_QCHANNEL', $args);
553             return {
554 0           Status => $DST0{$response->{DST0}},
555             QControl => $response->{CTRL} >> 4,
556             QAddress => $response->{CTRL} & 0xf,
557             Track => _from_bcd($response->{TNO}),
558             Index => _from_bcd($response->{INX}),
559             Minute => _from_bcd($response->{MIN}),
560             Second => _from_bcd($response->{SEC}),
561             Frame => _from_bcd($response->{FRM}),
562             AbsoluteMinute => _from_bcd($response->{AMIN}),
563             AbsoluteSecond => _from_bcd($response->{ASEC}),
564             AbsoluteFrame => _from_bcd($response->{AFRM}),
565             };
566             }
567              
568             sub toc_data_long {
569 0     0 0   my ($self, $drive, $args) = @_;
570 0 0         $args = {} unless ref($args) eq 'HASH';
571 0           $args->{DID} = $drive;
572 0           my $response = $self->command('ALL_TOC_DATA', $args);
573 0           return $response->{Data};
574             }
575              
576             sub toc_data_short {
577 0     0 0   my ($self, $drive, $args) = @_;
578 0 0         $args = {} unless ref($args) eq 'HASH';
579 0           $args->{DID} = $drive;
580 0           my $response = $self->command('SHORT_TOC_DATA', $args);
581 0           return $response->{Data};
582             }
583              
584             sub drive_play {
585 0     0 0   my ($self, $drive, $track, $args) = @_;
586 0 0         $args = {} unless ref($args) eq 'HASH';
587 0           $args->{DID} = $drive;
588 0           $args->{TNO} = _to_bcd($track);
589 0           $args->{MODE} = 0x29;
590 0           $args->{INX} = 1; # What is this?
591 0           foreach (qw(AMIN ASEC AFRM)) {
592 0 0         $args->{$_} = 0 unless exists $args->{$_};
593             }
594 0           return $self->command('PLAY_AUDIO', $args);
595             }
596              
597             sub drive_scan {
598 0     0 0   my ($self, $drive, $track, $args) = @_;
599 0 0         $args = {} unless ref($args) eq 'HASH';
600 0           $args->{DID} = $drive;
601 0           $args->{TNO} = _to_bcd($track);
602 0           $args->{MODE} = 0x29;
603 0           $args->{INX} = 1; # What is this?
604 0           foreach (qw(AMIN ASEC AFRM)) {
605 0 0         $args->{$_} = 0 unless exists $args->{$_};
606             }
607 0           return $self->command('AUDIO_SCAN', $args);
608             }
609              
610             sub drive_pause {
611 0     0 0   my ($self, $drive, $mode, $args) = @_;
612 0 0         $args = {} unless ref($args) eq 'HASH';
613 0           $args->{DID} = $drive;
614 0 0         $args->{MODE} = $mode ? 0x01 : 0x00;
615 0           return $self->command('PAUSE', $args);
616             }
617              
618             sub drive_stop {
619 0     0 0   my ($self, $drive, $args) = @_;
620 0 0         $args = {} unless ref($args) eq 'HASH';
621 0           $args->{DID} = $drive;
622 0           return $self->command('STOP', $args);
623             }
624              
625             sub drive_search {
626 0     0 0   my ($self, $drive, $track, $args) = @_;
627 0 0         $args = {} unless ref($args) eq 'HASH';
628 0           $args->{DID} = $drive;
629 0           $args->{TNO} = _to_bcd($track);
630 0           $args->{MODE} = 0x29;
631 0           $args->{INX} = 1; # What is this?
632 0           foreach (qw(AMIN ASEC AFRM)) {
633 0 0         $args->{$_} = 0 unless exists $args->{$_};
634             }
635 0           return $self->command('TRACK_SEARCH', $args);
636             }
637              
638             1;
639              
640             __END__