/[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

Contents of /3m-810.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 45 - (show annotations)
Tue Jun 23 13:29:10 2009 UTC (14 years, 10 months ago) by dpavlin
File MIME type: text/plain
File size: 17513 byte(s)
command-line option --http-server

1 #!/usr/bin/perl
2
3 use Device::SerialPort qw (:STAT);
4 use strict;
5 use warnings;
6
7 use Data::Dump qw/dump/;
8 use Carp qw/confess/;
9 use Getopt::Long;
10 use File::Slurp;
11 use JSON;
12
13 use IO::Socket::INET;
14
15 my $meteor_server = '192.168.1.13:4671';
16 my $meteor_fh;
17
18 sub meteor {
19 my @a = @_;
20 push @a, scalar localtime() if $a[0] =~ m{^info};
21
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;
110
111 my $device = "/dev/ttyUSB0";
112 my $baudrate = "19200";
113 my $databits = "8";
114 my $parity = "none";
115 my $stopbits = "1";
116 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 = {
129 'd500090400110a0500027250' => 'version?',
130 'd60007fe00000500c97b' => 'no tag in range',
131
132 'd6000ffe00000501e00401003123aa26941a' => 'tag #1',
133 'd6000ffe00000501e0040100017c0c388e2b' => 'rfid card',
134 'd6000ffe00000501e00401003123aa2875d4' => 'tag red-stripe',
135
136 'd60017fe00000502e00401003123aa26e0040100017c0c38cadb' => 'tag #1 + card',
137 'd60017fe00000502e00401003123aa26e00401003123aa283124' => 'tag #1 + tag red-stripe',
138 };
139
140 GetOptions(
141 'd|debug+' => \$debug,
142 'device=s' => \$device,
143 'baudrate=i' => \$baudrate,
144 'databits=i' => \$databits,
145 'parity=s' => \$parity,
146 'stopbits=i' => \$stopbits,
147 'handshake=s' => \$handshake,
148 'meteor=s' => \$meteor_server,
149 'http-server!' => \$http_server,
150 ) or die $!;
151
152 my $verbose = $debug > 0 ? $debug-- : 0;
153
154 =head1 NAME
155
156 3m-810 - support for 3M 810 RFID reader
157
158 =head1 SYNOPSIS
159
160 3m-810.pl --device /dev/ttyUSB0
161
162 =head1 DESCRIPTION
163
164 Communicate with 3M 810 RFID reader and document it's protocol
165
166 =head1 SEE ALSO
167
168 L<Device::SerialPort(3)>
169
170 L<perl(1)>
171
172 L<http://stackoverflow.com/questions/149617/how-could-i-guess-a-checksum-algorithm>
173
174 =head1 AUTHOR
175
176 Dobrica Pavlinusic <dpavlin@rot13.org> L<http://www.rot13.org/~dpavlin/>
177
178 =head1 COPYRIGHT AND LICENSE
179
180 This program is free software; you may redistribute it and/or modify
181 it under the same terms ans Perl itself.
182
183 =cut
184
185 my $tags_data;
186 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";
206 warn "using $device $handshake $baudrate $databits $parity $stopbits" if $debug;
207 $handshake=$port->handshake($handshake);
208 $baudrate=$port->baudrate($baudrate);
209 $databits=$port->databits($databits);
210 $parity=$port->parity($parity);
211 $stopbits=$port->stopbits($stopbits);
212
213 print "## using $device $baudrate $databits $parity $stopbits debug: $debug verbose: $verbose\n";
214
215 # Just in case: reset our timing and buffers
216 $port->lookclear();
217 $port->read_const_time(100);
218 $port->read_char_time(5);
219
220 # Turn on parity checking:
221 #$port->stty_inpck(1);
222 #$port->stty_istrip(1);
223
224 # initial hand-shake with device
225
226 cmd( 'D5 00 05 04 00 11 8C66', 'hw version',
227 'D5 00 09 04 00 11 0A 05 00 02 7250', sub {
228 my $hw_ver = join('.', unpack('CCCC', skip_assert(3) ));
229 print "hardware version $hw_ver\n";
230 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?',
234 'D6 00 0C 13 00 02 01 01 03 02 02 03 00 E778', sub { assert() } );
235
236 sub scan_for_tags {
237
238 my @tags;
239
240 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 );
255 die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;
256
257 push @tags, uc(unpack('H16', substr($tags, $_ * 8, 8))) foreach ( 0 .. $nr - 1 );
258 warn "## tags ",as_hex($tags), " [$tl] = ",dump( $tags ) if $debug;
259 print "$nr tags in range: ", join(',', @tags ) , "\n";
260
261 meteor( 'info-in-range', join(' ',@tags));
262
263 update_visible_tags( @tags );
264 }
265 }
266 );
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 {
284 my @tags = @_;
285
286 my $last_visible_tags = $visible_tags;
287 $visible_tags = {};
288
289 foreach my $tag ( @tags ) {
290 if ( ! defined $last_visible_tags->{$tag} ) {
291 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}++;
298 } else {
299 warn "## using cached data for $tag" if $debug;
300 }
301 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 ) {
314 my $data = delete $tags_data->{$tag};
315 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;
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 {
379 my ( $tag ) = @_;
380
381 confess "no tag?" unless $tag;
382
383 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(
406 "D6 00 0B 0A $tag 1234", "check security $tag",
407 "D6 00 0D 0A 00", sub {
408 my $rest = shift;
409 my $from_tag;
410 ( $from_tag, $security ) = ( substr($rest,0,8), substr($rest,8,1) );
411 die "security from other tag: ",as_hex( $from_tag ) if $from_tag ne str2bytes( $tag );
412 $security = as_hex( $security );
413 warn "# SECURITY $tag = $security\n";
414 }
415 );
416
417 print "TAG $tag ", dump(decode_tag( $tag ));
418 }
419
420 sub write_tag {
421 my ($tag) = @_;
422
423 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 }
465
466 sub secure_tag {
467 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;
485
486 for ( 1 .. 3 ) {
487
488 # ++-->type 00-0a
489 # D6 00 2A 04 E00401003123AA26 00 07 00 04 11 00 01 31 31 31 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1C D4
490 # D6 00 2A 04 E0 04 01 00 31 23 AA 26 00 07 00 04 11 00 06 32 32 32 32 32 32 32 32 32 32 32 00 00 00 00 00 00 00 00 00 00 00 00 00 32B7
491 # D6 00 2A 04 E0 04 01 00 31 23 AA 26 00 07 00 04 11 00 02 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 00 00 00 00 00 00 00 00 42 1F
492
493 cmd(' D6 00 2A 04 E00401003123AA26 00 07 00 04 11 00 01 30 30 30 30 30 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8843', "write offset 0, block: 7 -- 0000000000 $_" );
494 warn "D6 00 0D 04 00 E00401003123AA26 07 CFF1 -- ack 7 block?\n";
495
496 }
497 warn " D6 00 0F FE 00 00 05 01 E00401003123AA26 941A\n";
498
499 cmd( 'D6 00 05 FE 00 05 FA 40', "port-write scan $_" ) foreach ( 1 .. 2 );
500
501 cmd('D6 00 0C 09 E00401003123AA26 D7 3AF0', 'checkin?',
502 'D6 00 0C 09 00 E00401003123AA26 6A44 -- no?' );
503 cmd('D6 00 0C 09 E00401003123AA26 DA EB5D', 'checkout?',
504 'D6 00 0C 09 00 E00401003123AA26 6A44 -- no?' );
505
506 cmd('D6 00 26 04 E00401003123AA26 00 06 00 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 A98B', 'blank offset: 0 blocks: 6',
507 'D6 00 0D 04 00 E00401003123AA26 06 DFD0 -- ack 6 blocks' ) foreach ( 1 .. 3 );
508
509 undef $port;
510 print "Port closed\n";
511
512 sub writechunk
513 {
514 my $str=shift;
515 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;
519 }
520
521 sub as_hex {
522 my @out;
523 foreach my $str ( @_ ) {
524 my $hex = unpack( 'H*', $str );
525 $hex =~ s/(..)/$1 /g if length( $str ) > 2;
526 $hex =~ s/\s+$//;
527 push @out, $hex;
528 }
529 return join(' | ', @out);
530 }
531
532 sub read_bytes {
533 my ( $len, $desc ) = @_;
534 my $data = '';
535 while ( length( $data ) < $len ) {
536 my ( $c, $b ) = $port->read(1);
537 die "no bytes on port: $!" unless defined $b;
538 #warn "## got $c bytes: ", as_hex($b), "\n";
539 $data .= $b;
540 }
541 $desc ||= '?';
542 warn "#< ", as_hex($data), "\t$desc\n" if $debug;
543 return $data;
544 }
545
546 our $assert;
547
548 # my $rest = skip_assert( 3 );
549 sub skip_assert {
550 assert( 0, shift );
551 }
552
553 sub assert {
554 my ( $from, $to ) = @_;
555
556 $from ||= 0;
557 $to = length( $assert->{expect} ) if ! defined $to;
558
559 my $p = substr( $assert->{payload}, $from, $to );
560 my $e = substr( $assert->{expect}, $from, $to );
561 warn "EXPECTED ",as_hex($e), " GOT ", as_hex($p), " [$from-$to] in ",dump( $assert ), "\n" if $e ne $p;
562
563 # return the rest
564 return substr( $assert->{payload}, $to );
565 }
566
567 use Digest::CRC;
568
569 sub crcccitt {
570 my $bytes = shift;
571 my $crc = Digest::CRC->new(
572 # midified CCITT to xor with 0xffff instead of 0x0000
573 width => 16, init => 0xffff, xorout => 0xffff, refout => 0, poly => 0x1021, refin => 0,
574 ) or die $!;
575 $crc->add( $bytes );
576 pack('n', $crc->digest);
577 }
578
579 # my $checksum = checksum( $bytes );
580 # my $checksum = checksum( $bytes, $original_checksum );
581 sub checksum {
582 my ( $bytes, $checksum ) = @_;
583
584 my $len = ord(substr($bytes,2,1));
585 my $len_real = length($bytes) - 1;
586
587 if ( $len_real != $len ) {
588 print "length wrong: $len_real != $len\n";
589 $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 ) {
596 print "checksum doesn't match: ", as_hex($xor), " != ", as_hex($checksum), " data: ", as_hex($bytes), "\n";
597 return $bytes . $xor;
598 }
599 return $bytes . $checksum;
600 }
601
602 our $dispatch;
603
604 sub readchunk {
605 # sleep 1; # FIXME remove
606
607 # read header of packet
608 my $header = read_bytes( 2, 'header' );
609 my $length = read_bytes( 1, 'length' );
610 my $len = ord($length);
611 my $data = read_bytes( $len, 'data' );
612
613 my $payload = substr( $data, 0, -2 );
614 my $payload_len = length($data);
615 warn "## payload too short $payload_len != $len\n" if $payload_len != $len;
616
617 my $checksum = substr( $data, -2, 2 );
618 checksum( $header . $length . $payload , $checksum );
619
620 print "<< ",as_hex( $header ), " [$len] ", as_hex( $payload ), " | sum: ",as_hex($checksum),"\n" if $verbose;
621
622 $assert->{len} = $len;
623 $assert->{payload} = $payload;
624
625 my $full = $header . $length . $data; # full
626 # find longest match for incomming data
627 my ($to) = grep {
628 my $match = substr($payload,0,length($_));
629 m/^\Q$match\E/
630 } sort { length($a) <=> length($b) } keys %$dispatch;
631 warn "?? payload dispatch to ",dump( $payload, $dispatch, $to ) if $debug;
632
633 if ( defined $to ) {
634 my $rest = substr( $payload, length($to) ) if length($to) < length($payload);
635 warn "## DISPATCH payload to with rest", dump( $payload, $to, $rest ) if $debug;
636 $dispatch->{ $to }->( $rest );
637 } else {
638 print "NO DISPATCH for ",dump( $full ),"\n";
639 }
640
641 return $data;
642 }
643
644 sub str2bytes {
645 my $str = shift || confess "no str?";
646 my $b = $str;
647 $b =~ s/\s+//g;
648 $b =~ s/(..)/\\x$1/g;
649 $b = "\"$b\"";
650 my $bytes = eval $b;
651 die $@ if $@;
652 warn "## str2bytes( $str ) => $b => ",as_hex($bytes) if $debug;
653 return $bytes;
654 }
655
656 sub cmd {
657 my $cmd = shift || confess "no cmd?";
658 my $cmd_desc = shift || confess "no description?";
659 my @expect = @_;
660
661 my $bytes = str2bytes( $cmd );
662
663 # fix checksum if needed
664 $bytes = checksum( substr( $bytes, 0, -2 ), substr( $bytes, -2, 2 ) );
665
666 warn ">> ", as_hex( $bytes ), "\t## $cmd_desc\n" if $verbose;
667 $assert->{send} = $cmd;
668 writechunk( $bytes );
669
670 while ( @expect ) {
671 my $pattern = str2bytes( shift @expect ) || confess "no pattern?";
672 my $coderef = shift @expect || confess "no coderef?";
673 confess "not coderef" unless ref $coderef eq 'CODE';
674
675 next if defined $dispatch->{ $pattern };
676
677 $dispatch->{ substr($pattern,3) } = $coderef;
678 warn "++ dispatch ", as_hex($pattern) ,dump( $dispatch ) if $debug;
679 }
680
681 readchunk;
682 }
683

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.26