/[RFID]/3m-810.pl
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Diff of /3m-810.pl

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 23 by dpavlin, Sat Mar 28 03:47:10 2009 UTC revision 45 by dpavlin, Tue Jun 23 13:29:10 2009 UTC
# Line 7  use warnings; Line 7  use warnings;
7  use Data::Dump qw/dump/;  use Data::Dump qw/dump/;
8  use Carp qw/confess/;  use Carp qw/confess/;
9  use Getopt::Long;  use Getopt::Long;
10    use File::Slurp;
11    use JSON;
12    
13  use IO::Socket::INET;  use IO::Socket::INET;
14    
15  my $meteor = IO::Socket::INET->new( '192.168.1.13:4671' ) || die "can't connect to meteor: $!";  my $meteor_server = '192.168.1.13:4671';
16    my $meteor_fh;
17    
18  sub meteor {  sub meteor {
19          my ( $item, $html ) = @_;          my @a = @_;
20          warn ">> meteor $item $html\n";          push @a, scalar localtime() if $a[0] =~ m{^info};
21          print $meteor "ADDMESSAGE test $item|" . localtime() . "<br>$html\n";  
22            if ( ! defined $meteor_fh ) {
23                    if ( $meteor_fh =
24                                    IO::Socket::INET->new(
25                                            PeerAddr => $meteor_server,
26                                            Timeout => 1,
27                                    )
28                    ) {
29                            warn "# meteor connected to $meteor_server";
30                    } else {
31                            warn "can't connect to meteor $meteor_server: $!";
32                            $meteor_fh = 0;
33                    }
34            }
35    
36            if ( $meteor_fh ) {
37                    warn ">> meteor ",dump( @a );
38                    print $meteor_fh "ADDMESSAGE test ",join('|',@a),"\n"
39            }
40    }
41    
42    my $listen_port = 9000;                  # pick something not in use
43    sub http_server {
44    
45            my $server = IO::Socket::INET->new(
46                    Proto     => 'tcp',
47                    LocalPort => $listen_port,
48                    Listen    => SOMAXCONN,
49                    Reuse     => 1
50            );
51                                                                      
52            die "can't setup server" unless $server;
53    
54            print "Server $0 accepting clients at http://localhost:$listen_port/\n";
55    
56            sub static {
57                    my ($client,$path) = @_;
58    
59                    $path = "www/$path";
60    
61                    return unless -e $path;
62    
63                    my $type = 'text/plain';
64                    $type = 'text/html' if $path =~ m{\.htm};
65                    $type = 'application/javascript' if $path =~ m{\.js};
66    
67                    print $client "HTTP/1.0 200 OK\r\nContent-Type: $type\r\n\r\n";
68                    open(my $html, $path);
69                    while(<$html>) {
70                            print $client $_;
71                    }
72                    close($html);
73    
74                    return $path;
75            }
76    
77            while (my $client = $server->accept()) {
78                    $client->autoflush(1);
79                    my $request = <$client>;
80    
81                    warn "<< $request\n";
82    
83                    if ($request =~ m{^GET (/.*) HTTP/1.[01]}) {
84                            my $method = $1;
85                            if ( my $path = static( $client,$1 ) ) {
86                                    warn ">> $path";
87                            } elsif ( $method =~ m{/scan} ) {
88                                    my $callback = $1 if $method =~ m{\?callback=([^&;]+)};
89                                    my $tags = scan_for_tags();
90                                    my $json;
91                                    map {
92                                            my $d = decode_tag($_);
93                                            $d->{sid} = $_;
94                                            push @{ $json->{tags} },  $d;
95                                    } keys %$tags;
96                                    print $client "HTTP/1.0 200 OK\r\nContent-Type: application/x-javascript\r\n\r\n$callback(", to_json($json), ")\r\n";
97                            } else {
98                                    print $client "HTTP/1.0 404 Unkown method\r\n";
99                            }
100                    } else {
101                            print $client "HTTP/1.0 500 No method\r\n";
102                    }
103                    close $client;
104            }
105    
106            die "server died";
107  }  }
108    
109  my $debug = 0;  my $debug = 0;
# Line 27  my $parity       = "none"; Line 115  my $parity       = "none";
115  my $stopbits  = "1";  my $stopbits  = "1";
116  my $handshake = "none";  my $handshake = "none";
117    
118    my $program_path = './program/';
119    my $secure_path = './secure/';
120    
121    # http server
122    my $http_server = 1;
123    
124    # 3M defaults: 8,4
125    my $max_rfid_block = 16;
126    my $read_blocks = 8;
127    
128  my $response = {  my $response = {
129          'd500090400110a0500027250'                              => 'version?',          'd500090400110a0500027250'                              => 'version?',
130          'd60007fe00000500c97b'                                  => 'no tag in range',          'd60007fe00000500c97b'                                  => 'no tag in range',
# Line 47  GetOptions( Line 145  GetOptions(
145          'parity=s'    => \$parity,          'parity=s'    => \$parity,
146          'stopbits=i'  => \$stopbits,          'stopbits=i'  => \$stopbits,
147          'handshake=s' => \$handshake,          'handshake=s' => \$handshake,
148            'meteor=s'    => \$meteor_server,
149            'http-server!' => \$http_server,
150  ) or die $!;  ) or die $!;
151    
152  my $verbose = $debug > 0 ? $debug-- : 0;  my $verbose = $debug > 0 ? $debug-- : 0;
# Line 85  it under the same terms ans Perl itself. Line 185  it under the same terms ans Perl itself.
185  my $tags_data;  my $tags_data;
186  my $visible_tags;  my $visible_tags;
187    
188    my $item_type = {
189            1 => 'Book',
190            6 => 'CD/CD ROM',
191            2 => 'Magazine',
192            13 => 'Book with Audio Tape',
193            9 => 'Book with CD/CD ROM',
194            0 => 'Other',
195    
196            5 => 'Video',
197            4 => 'Audio Tape',
198            3 => 'Bound Journal',
199            8 => 'Book with Diskette',
200            7 => 'Diskette',
201    };
202    
203    warn "## known item type: ",dump( $item_type ) if $debug;
204    
205  my $port=new Device::SerialPort($device) || die "can't open serial port $device: $!\n";  my $port=new Device::SerialPort($device) || die "can't open serial port $device: $!\n";
206  warn "using $device $handshake $baudrate $databits $parity $stopbits" if $debug;  warn "using $device $handshake $baudrate $databits $parity $stopbits" if $debug;
207  $handshake=$port->handshake($handshake);  $handshake=$port->handshake($handshake);
# Line 110  cmd( 'D5 00  05   04 00 11 Line 227  cmd( 'D5 00  05   04 00 11
227       'D5 00  09   04 00 11   0A 05 00 02   7250', sub {       'D5 00  09   04 00 11   0A 05 00 02   7250', sub {
228          my $hw_ver = join('.', unpack('CCCC', skip_assert(3) ));          my $hw_ver = join('.', unpack('CCCC', skip_assert(3) ));
229          print "hardware version $hw_ver\n";          print "hardware version $hw_ver\n";
230          meteor( -1, "Found reader $hw_ver" );          meteor( 'info', "Found reader hardware $hw_ver" );
231  });  });
232    
233  cmd( 'D6 00  0C   13  04  01 00  02 00  03 00  04 00   AAF2','FIXME: stats?',  cmd( 'D6 00  0C   13  04  01 00  02 00  03 00  04 00   AAF2','FIXME: stats?',
234       'D6 00  0C   13  00  02 01 01 03 02 02 03  00     E778', sub { assert() }  );       'D6 00  0C   13  00  02 01 01 03 02 02 03  00     E778', sub { assert() }  );
235    
236  # start scanning for tags  sub scan_for_tags {
237    
238  cmd( 'D6 00  05   FE     00  05         FA40', "scan for tags, retry $_",          my @tags;
          'D6 00  0F   FE  00 00  05 ', sub { # 01 E00401003123AA26  941A         # seen, serial length: 8  
                 my $rest = shift || die "no rest?";  
                 my $nr = ord( substr( $rest, 0, 1 ) );  
   
                 if ( ! $nr ) {  
                         print "no tags in range\n";  
                         update_visible_tags();  
                         meteor( -1, "No tags in range" );  
                 } else {  
239    
240                          my $tags = substr( $rest, 1 );          cmd( 'D6 00  05   FE     00  05         FA40', "scan for tags, retry $_",
241                     'D6 00  0F   FE  00 00  05 ', sub { # 01 E00401003123AA26  941A         # seen, serial length: 8
242                            my $rest = shift || die "no rest?";
243                            my $nr = ord( substr( $rest, 0, 1 ) );
244    
245                            if ( ! $nr ) {
246                                    print "no tags in range\n";
247                                    update_visible_tags();
248                                    meteor( 'info-none-in-range' );
249                                    $tags_data = {};
250                            } else {
251    
252                                    my $tags = substr( $rest, 1 );
253    
254                          my $tl = length( $tags );                                  my $tl = length( $tags );
255                          die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;                                  die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;
256    
257                          my @tags;                                  push @tags, uc(unpack('H16', substr($tags, $_ * 8, 8))) foreach ( 0 .. $nr - 1 );
258                          push @tags, uc(unpack('H16', substr($tags, $_ * 8, 8))) foreach ( 0 .. $nr - 1 );                                  warn "## tags ",as_hex($tags), " [$tl] = ",dump( $tags ) if $debug;
259                          warn "## tags ",as_hex($tags), " [$tl] = ",dump( $tags ) if $debug;                                  print "$nr tags in range: ", join(',', @tags ) , "\n";
                         print "$nr tags in range: ", join(',', @tags ) , "\n";  
260    
261                          update_visible_tags( @tags );                                  meteor( 'info-in-range', join(' ',@tags));
262    
263                          my $html = join('', map { "<li><tt>$_</tt>" } @tags);                                  update_visible_tags( @tags );
264                          meteor( 0, "Tags:<ul>$html</ul>" );                          }
265                  }                  }
266          }          );
 ) foreach ( 1 .. 1000 );  
267    
268            warn "## tags: ",dump( @tags );
269            return $tags_data;
270    
271    }
272    
273    # start scanning for tags
274    
275    if ( $http_server ) {
276            http_server;
277    } else {
278            scan_for_tags while 1;
279    }
280    
281    die "over and out";
282    
283  sub update_visible_tags {  sub update_visible_tags {
284          my @tags = @_;          my @tags = @_;
# Line 157  sub update_visible_tags { Line 288  sub update_visible_tags {
288    
289          foreach my $tag ( @tags ) {          foreach my $tag ( @tags ) {
290                  if ( ! defined $last_visible_tags->{$tag} ) {                  if ( ! defined $last_visible_tags->{$tag} ) {
291                          read_tag( $tag );                          if ( defined $tags_data->{$tag} ) {
292    #                               meteor( 'in-range', $tag );
293                            } else {
294                                    meteor( 'read', $tag );
295                                    read_tag( $tag );
296                            }
297                          $visible_tags->{$tag}++;                          $visible_tags->{$tag}++;
298                  } else {                  } else {
299                          warn "## using cached data for $tag" if $debug;                          warn "## using cached data for $tag" if $debug;
300                  }                  }
301                  delete $last_visible_tags->{$tag}; # leave just missing tags                  delete $last_visible_tags->{$tag}; # leave just missing tags
302    
303                    if ( -e "$program_path/$tag" ) {
304                                    meteor( 'write', $tag );
305                                    write_tag( $tag );
306                    }
307                    if ( -e "$secure_path/$tag" ) {
308                                    meteor( 'secure', $tag );
309                                    secure_tag( $tag );
310                    }
311          }          }
312    
313          foreach my $tag ( keys %$last_visible_tags ) {          foreach my $tag ( keys %$last_visible_tags ) {
314                  my $data = delete $tags_data->{$tag};                  my $data = delete $tags_data->{$tag};
315                  print "removed tag $tag with data ",dump( $data ),"\n";                  print "removed tag $tag with data ",dump( $data ),"\n";
316                    meteor( 'removed', $tag );
317          }          }
318    
319          warn "## update_visible_tags(",dump( @tags ),") = ",dump( $visible_tags )," removed: ",dump( $last_visible_tags ), " data: ",dump( $tags_data ) if $debug;          warn "## update_visible_tags(",dump( @tags ),") = ",dump( $visible_tags )," removed: ",dump( $last_visible_tags ), " data: ",dump( $tags_data ) if $debug;
320  }  }
321    
322    my $tag_data_block;
323    
324    sub read_tag_data {
325            my ($start_block,$rest) = @_;
326            die "no rest?" unless $rest;
327    
328            my $last_block = 0;
329    
330            warn "## DATA [$start_block] ", dump( $rest ) if $debug;
331            my $tag = uc(unpack('H16',substr( $rest, 0, 8 )));
332            my $blocks = ord(substr($rest,8,1));
333            $rest = substr($rest,9); # leave just data blocks
334            foreach my $nr ( 0 .. $blocks - 1 ) {
335                    my $block = substr( $rest, $nr * 6, 6 );
336                    warn "## block ",as_hex( $block ) if $debug;
337                    my $ord   = unpack('v',substr( $block, 0, 2 ));
338                    my $expected_ord = $nr + $start_block;
339                    warn "got block $ord, expected block $expected_ord from ",dump( $block ) if $ord != $expected_ord;
340                    my $data  = substr( $block, 2 );
341                    die "data payload should be 4 bytes" if length($data) != 4;
342                    warn sprintf "## tag %9s %02d: %s |%-4s|\n", $tag, $ord, as_hex( $data ), $data;
343                    $tag_data_block->{$tag}->[ $ord ] = $data;
344                    $last_block = $ord;
345            }
346            $tags_data->{ $tag } = join('', @{ $tag_data_block->{$tag} });
347    
348            my $item_type_nr = ord(substr( $tags_data->{$tag}, 3, 1 ));
349            print "DATA $tag ",dump( $tags_data ), " item type: ", ( $item_type->{ $item_type_nr } || "UNKWOWN '$item_type_nr'" ), "\n";
350    
351            return $last_block + 1;
352    }
353    
354    sub decode_tag {
355            my $tag = shift;
356    
357            my $data = $tags_data->{$tag} || die "no data for $tag";
358    
359            my ( $u1, $set_item, $u2, $type, $content, $br_lib, $custom ) = unpack('C4Z16Nl>',$data);
360            my $hash = {
361                    u1 => $u1,
362                    u2 => $u2,
363                    set => ( $set_item & 0xf0 ) >> 4,
364                    total => ( $set_item & 0x0f ),
365    
366                    type => $type,
367                    content => $content,
368    
369                    branch => $br_lib >> 20,
370                    library => $br_lib & 0x000fffff,
371    
372                    custom => $custom,
373            };
374    
375            return $hash;
376    }
377    
378  sub read_tag {  sub read_tag {
379          my ( $tag ) = @_;          my ( $tag ) = @_;
380    
381          confess "no tag?" unless $tag;          confess "no tag?" unless $tag;
382    
         return if defined $tags_data->{$tag};  
   
383          print "read_tag $tag\n";          print "read_tag $tag\n";
384    
385            my $start_block = 0;
386    
387            while ( $start_block < $max_rfid_block ) {
388    
389                    cmd(
390                             sprintf( "D6 00  0D  02      $tag   %02x   %02x     ffff", $start_block, $read_blocks ),
391                                    "read $tag offset: $start_block blocks: $read_blocks",
392                            "D6 00  1F  02 00", sub { # $tag  03   00 00   04 11 00 01   01 00   31 32 33 34   02 00   35 36 37 38    531F\n";
393                                    $start_block = read_tag_data( $start_block, @_ );
394                                    warn "# read tag upto $start_block\n";
395                            },
396                            "D6 00  0F  FE  00 00  05 01   $tag    941A", sub {
397                                    print "FIXME: tag $tag ready? (expected block read instead)\n";
398                            },
399                    );
400    
401            }
402    
403            my $security;
404    
405          cmd(          cmd(
406                  "D6 00  0D  02      $tag   00   03     1CC4", 'read $tag offset: 0 blocks: 3',                  "D6 00 0B 0A $tag 1234", "check security $tag",
407                  "D6 00  0F  FE  00 00  05 01   $tag    941A", sub {                  "D6 00 0D 0A 00", sub {
408                          print "FIXME: tag $tag ready?\n";                          my $rest = shift;
409                  },                          my $from_tag;
410                  "D6 00  1F  02 00", sub { # $tag  03   00 00   04 11 00 01   01 00   31 32 33 34   02 00   35 36 37 38    531F\n";                          ( $from_tag, $security ) = ( substr($rest,0,8), substr($rest,8,1) );
411                          my $rest = shift || die "no rest?";                          die "security from other tag: ",as_hex( $from_tag ) if $from_tag ne str2bytes( $tag );
412                          warn "## DATA ", dump( $rest ) if $debug;                          $security = as_hex( $security );
413                          my $tag = uc(unpack('H16',substr( $rest, 0, 8 )));                          warn "# SECURITY $tag = $security\n";
                         my $blocks = ord(substr($rest,8,1));  
                         $rest = substr($rest,9); # leave just data blocks  
                         my @data;  
                         foreach my $nr ( 0 .. $blocks - 1 ) {  
                                 my $block = substr( $rest, $nr * 6, 6 );  
                                 warn "## block ",as_hex( $block ) if $debug;  
                                 my $ord   = unpack('v',substr( $block, 0, 2 ));  
                                 die "got block $ord, expected block $nr from ",dump( $block ) if $ord != $nr;  
                                 my $data  = substr( $block, 2 );  
                                 die "data payload should be 4 bytes" if length($data) != 4;  
                                 warn sprintf "## tag %08s %02d %s |%-4s|\n", $tag, $ord, as_hex( $data ), $data;  
                                 $data[ $ord ] = $data;  
                         }  
                         $tags_data->{ $tag } = join('', @data);  
                         print "DATA $tag ",dump( $tags_data ), "\n";  
414                  }                  }
415          );          );
416    
417          #        D6 00  1F  02 00   $tag   03   00 00   04 11 00 01   01 00   30 30 30 30   02 00   30 30 30 30    E5F4          print "TAG $tag ", dump(decode_tag( $tag ));
418  if (0) {  }
419          cmd(    "D6 00  0D  02      $tag   03   04     3970", 'read offset: 3 blocks: 4' );  
420    sub write_tag {
421          #        D6 00  25  02 00   $tag   04                         03 00   30 30 00 00   04 00   00 00 00 00            my ($tag) = @_;
422          #                           $tag                              05 00   00 00 00 00   06 00   00 00 00 00    B9BA  
423          warn "?? D6 00  25  02 00   $tag   04                         03 00   39 30 31 32   04 00   ....\n";          my $path = "$program_path/$tag";
424    
425            my $data = read_file( $path );
426            my $hex_data;
427    
428            if ( $data =~ s{^hex\s+}{} ) {
429                    $hex_data = $data;
430                    $hex_data =~ s{\s+}{}g;
431            } else {
432    
433                    $data .= "\0" x ( 4 - ( length($data) % 4 ) );
434    
435                    my $max_len = $max_rfid_block * 4;
436    
437                    if ( length($data) > $max_len ) {
438                            $data = substr($data,0,$max_len);
439                            warn "strip content to $max_len bytes\n";
440                    }
441    
442                    $hex_data = unpack('H*', $data);
443            }
444    
445            my $len = length($hex_data) / 2;
446            # pad to block size
447            $hex_data .= '00' x ( 4 - $len % 4 );
448            my $blocks = sprintf('%02x', length($hex_data) / 4);
449    
450            print "write_tag $tag = ",dump( $data ), " [$len/$blocks] == $hex_data\n";
451    
452            cmd(
453                    "d6 00  ff  04  $tag  00 $blocks 00  $hex_data  ffff", "write $tag",
454                    "d6 00  0d  04 00  $tag  $blocks  afb1", sub { assert() },
455            ); # foreach ( 1 .. 3 ); # XXX 3m software does this three times!
456    
457            my $to = $path;
458            $to .= '.' . time();
459    
460            rename $path, $to;
461            print ">> $to\n";
462    
463            delete $tags_data->{$tag};      # force re-read of tag
464  }  }
         warn "?? D6 00  0F  FE  00 00  05 01   $tag  941A ##### ready?\n";  
465    
466          my $item = unpack('H*', substr($tag,-8) ) % 100000;  sub secure_tag {
467          meteor( $item, "Loading $item" );          my ($tag) = @_;
468    
469            my $path = "$secure_path/$tag";
470            my $data = substr(read_file( $path ),0,2);
471    
472            cmd(
473                    "d6 00  0c  09  $tag $data 1234", "secure $tag -> $data",
474                    "d6 00  0c  09 00  $tag  1234", sub { assert() },
475            );
476    
477            my $to = $path;
478            $to .= '.' . time();
479    
480            rename $path, $to;
481            print ">> $to\n";
482  }  }
483    
484  exit;  exit;
# Line 257  sub writechunk Line 513  sub writechunk
513  {  {
514          my $str=shift;          my $str=shift;
515          my $count = $port->write($str);          my $count = $port->write($str);
516            my $len = length($str);
517            die "wrong write length $count != $len in ",as_hex( $str ) if $count != $len;
518          print "#> ", as_hex( $str ), "\t[$count]\n" if $debug;          print "#> ", as_hex( $str ), "\t[$count]\n" if $debug;
519  }  }
520    
# Line 276  sub read_bytes { Line 534  sub read_bytes {
534          my $data = '';          my $data = '';
535          while ( length( $data ) < $len ) {          while ( length( $data ) < $len ) {
536                  my ( $c, $b ) = $port->read(1);                  my ( $c, $b ) = $port->read(1);
537                    die "no bytes on port: $!" unless defined $b;
538                  #warn "## got $c bytes: ", as_hex($b), "\n";                  #warn "## got $c bytes: ", as_hex($b), "\n";
539                  $data .= $b;                  $data .= $b;
540          }          }
# Line 322  sub crcccitt { Line 581  sub crcccitt {
581  sub checksum {  sub checksum {
582          my ( $bytes, $checksum ) = @_;          my ( $bytes, $checksum ) = @_;
583    
         my $xor = crcccitt( substr($bytes,1) ); # skip D6  
         warn "## checksum ",dump( $bytes, $xor, $checksum ) if $debug;  
   
584          my $len = ord(substr($bytes,2,1));          my $len = ord(substr($bytes,2,1));
585          my $len_real = length($bytes) - 1;          my $len_real = length($bytes) - 1;
586    
587          if ( $len_real != $len ) {          if ( $len_real != $len ) {
588                  print "length wrong: $len_real != $len\n";                  print "length wrong: $len_real != $len\n";
589                  $bytes = substr($bytes,0,2) . chr($len_real) . substr($bytes,4);                  $bytes = substr($bytes,0,2) . chr($len_real) . substr($bytes,3);
590          }          }
591    
592            my $xor = crcccitt( substr($bytes,1) ); # skip D6
593            warn "## checksum ",dump( $bytes, $xor, $checksum ) if $debug;
594    
595          if ( defined $checksum && $xor ne $checksum ) {          if ( defined $checksum && $xor ne $checksum ) {
596                  print "checksum doesn't match: ", as_hex($xor), " != ", as_hex($checksum), " data: ", as_hex($bytes), "\n";                  print "checksum doesn't match: ", as_hex($xor), " != ", as_hex($checksum), " data: ", as_hex($bytes), "\n";
597                  return $bytes . $xor;                  return $bytes . $xor;
# Line 343  sub checksum { Line 602  sub checksum {
602  our $dispatch;  our $dispatch;
603    
604  sub readchunk {  sub readchunk {
605          sleep 1;        # FIXME remove  #       sleep 1;        # FIXME remove
606    
607          # read header of packet          # read header of packet
608          my $header = read_bytes( 2, 'header' );          my $header = read_bytes( 2, 'header' );
# Line 372  sub readchunk { Line 631  sub readchunk {
631          warn "?? payload dispatch to ",dump( $payload, $dispatch, $to ) if $debug;          warn "?? payload dispatch to ",dump( $payload, $dispatch, $to ) if $debug;
632    
633          if ( defined $to ) {          if ( defined $to ) {
634                  my $rest = substr( $payload, length($to) );                  my $rest = substr( $payload, length($to) ) if length($to) < length($payload);
635                  warn "## DISPATCH payload to with rest", dump( $payload, $to, $rest ) if $debug;                  warn "## DISPATCH payload to with rest", dump( $payload, $to, $rest ) if $debug;
636                  $dispatch->{ $to }->( $rest );                  $dispatch->{ $to }->( $rest );
637          } else {          } else {

Legend:
Removed from v.23  
changed lines
  Added in v.45

  ViewVC Help
Powered by ViewVC 1.1.26