/[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 44 - (show annotations)
Tue Jun 23 13:10:18 2009 UTC (14 years, 10 months ago) by dpavlin
File MIME type: text/plain
File size: 17479 byte(s)
/scan now returns JSONP

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 ) or die $!;
150
151 my $verbose = $debug > 0 ? $debug-- : 0;
152
153 =head1 NAME
154
155 3m-810 - support for 3M 810 RFID reader
156
157 =head1 SYNOPSIS
158
159 3m-810.pl --device /dev/ttyUSB0
160
161 =head1 DESCRIPTION
162
163 Communicate with 3M 810 RFID reader and document it's protocol
164
165 =head1 SEE ALSO
166
167 L<Device::SerialPort(3)>
168
169 L<perl(1)>
170
171 L<http://stackoverflow.com/questions/149617/how-could-i-guess-a-checksum-algorithm>
172
173 =head1 AUTHOR
174
175 Dobrica Pavlinusic <dpavlin@rot13.org> L<http://www.rot13.org/~dpavlin/>
176
177 =head1 COPYRIGHT AND LICENSE
178
179 This program is free software; you may redistribute it and/or modify
180 it under the same terms ans Perl itself.
181
182 =cut
183
184 my $tags_data;
185 my $visible_tags;
186
187 my $item_type = {
188 1 => 'Book',
189 6 => 'CD/CD ROM',
190 2 => 'Magazine',
191 13 => 'Book with Audio Tape',
192 9 => 'Book with CD/CD ROM',
193 0 => 'Other',
194
195 5 => 'Video',
196 4 => 'Audio Tape',
197 3 => 'Bound Journal',
198 8 => 'Book with Diskette',
199 7 => 'Diskette',
200 };
201
202 warn "## known item type: ",dump( $item_type ) if $debug;
203
204 my $port=new Device::SerialPort($device) || die "can't open serial port $device: $!\n";
205 warn "using $device $handshake $baudrate $databits $parity $stopbits" if $debug;
206 $handshake=$port->handshake($handshake);
207 $baudrate=$port->baudrate($baudrate);
208 $databits=$port->databits($databits);
209 $parity=$port->parity($parity);
210 $stopbits=$port->stopbits($stopbits);
211
212 print "## using $device $baudrate $databits $parity $stopbits debug: $debug verbose: $verbose\n";
213
214 # Just in case: reset our timing and buffers
215 $port->lookclear();
216 $port->read_const_time(100);
217 $port->read_char_time(5);
218
219 # Turn on parity checking:
220 #$port->stty_inpck(1);
221 #$port->stty_istrip(1);
222
223 # initial hand-shake with device
224
225 cmd( 'D5 00 05 04 00 11 8C66', 'hw version',
226 'D5 00 09 04 00 11 0A 05 00 02 7250', sub {
227 my $hw_ver = join('.', unpack('CCCC', skip_assert(3) ));
228 print "hardware version $hw_ver\n";
229 meteor( 'info', "Found reader hardware $hw_ver" );
230 });
231
232 cmd( 'D6 00 0C 13 04 01 00 02 00 03 00 04 00 AAF2','FIXME: stats?',
233 'D6 00 0C 13 00 02 01 01 03 02 02 03 00 E778', sub { assert() } );
234
235 sub scan_for_tags {
236
237 my @tags;
238
239 cmd( 'D6 00 05 FE 00 05 FA40', "scan for tags, retry $_",
240 'D6 00 0F FE 00 00 05 ', sub { # 01 E00401003123AA26 941A # seen, serial length: 8
241 my $rest = shift || die "no rest?";
242 my $nr = ord( substr( $rest, 0, 1 ) );
243
244 if ( ! $nr ) {
245 print "no tags in range\n";
246 update_visible_tags();
247 meteor( 'info-none-in-range' );
248 $tags_data = {};
249 } else {
250
251 my $tags = substr( $rest, 1 );
252
253 my $tl = length( $tags );
254 die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;
255
256 push @tags, uc(unpack('H16', substr($tags, $_ * 8, 8))) foreach ( 0 .. $nr - 1 );
257 warn "## tags ",as_hex($tags), " [$tl] = ",dump( $tags ) if $debug;
258 print "$nr tags in range: ", join(',', @tags ) , "\n";
259
260 meteor( 'info-in-range', join(' ',@tags));
261
262 update_visible_tags( @tags );
263 }
264 }
265 );
266
267 warn "## tags: ",dump( @tags );
268 return $tags_data;
269
270 }
271
272 # start scanning for tags
273
274 if ( $http_server ) {
275 http_server;
276 } else {
277 scan_for_tags while 1;
278 }
279
280 die "over and out";
281
282 sub update_visible_tags {
283 my @tags = @_;
284
285 my $last_visible_tags = $visible_tags;
286 $visible_tags = {};
287
288 foreach my $tag ( @tags ) {
289 if ( ! defined $last_visible_tags->{$tag} ) {
290 if ( defined $tags_data->{$tag} ) {
291 # meteor( 'in-range', $tag );
292 } else {
293 meteor( 'read', $tag );
294 read_tag( $tag );
295 }
296 $visible_tags->{$tag}++;
297 } else {
298 warn "## using cached data for $tag" if $debug;
299 }
300 delete $last_visible_tags->{$tag}; # leave just missing tags
301
302 if ( -e "$program_path/$tag" ) {
303 meteor( 'write', $tag );
304 write_tag( $tag );
305 }
306 if ( -e "$secure_path/$tag" ) {
307 meteor( 'secure', $tag );
308 secure_tag( $tag );
309 }
310 }
311
312 foreach my $tag ( keys %$last_visible_tags ) {
313 my $data = delete $tags_data->{$tag};
314 print "removed tag $tag with data ",dump( $data ),"\n";
315 meteor( 'removed', $tag );
316 }
317
318 warn "## update_visible_tags(",dump( @tags ),") = ",dump( $visible_tags )," removed: ",dump( $last_visible_tags ), " data: ",dump( $tags_data ) if $debug;
319 }
320
321 my $tag_data_block;
322
323 sub read_tag_data {
324 my ($start_block,$rest) = @_;
325 die "no rest?" unless $rest;
326
327 my $last_block = 0;
328
329 warn "## DATA [$start_block] ", dump( $rest ) if $debug;
330 my $tag = uc(unpack('H16',substr( $rest, 0, 8 )));
331 my $blocks = ord(substr($rest,8,1));
332 $rest = substr($rest,9); # leave just data blocks
333 foreach my $nr ( 0 .. $blocks - 1 ) {
334 my $block = substr( $rest, $nr * 6, 6 );
335 warn "## block ",as_hex( $block ) if $debug;
336 my $ord = unpack('v',substr( $block, 0, 2 ));
337 my $expected_ord = $nr + $start_block;
338 warn "got block $ord, expected block $expected_ord from ",dump( $block ) if $ord != $expected_ord;
339 my $data = substr( $block, 2 );
340 die "data payload should be 4 bytes" if length($data) != 4;
341 warn sprintf "## tag %9s %02d: %s |%-4s|\n", $tag, $ord, as_hex( $data ), $data;
342 $tag_data_block->{$tag}->[ $ord ] = $data;
343 $last_block = $ord;
344 }
345 $tags_data->{ $tag } = join('', @{ $tag_data_block->{$tag} });
346
347 my $item_type_nr = ord(substr( $tags_data->{$tag}, 3, 1 ));
348 print "DATA $tag ",dump( $tags_data ), " item type: ", ( $item_type->{ $item_type_nr } || "UNKWOWN '$item_type_nr'" ), "\n";
349
350 return $last_block + 1;
351 }
352
353 sub decode_tag {
354 my $tag = shift;
355
356 my $data = $tags_data->{$tag} || die "no data for $tag";
357
358 my ( $u1, $set_item, $u2, $type, $content, $br_lib, $custom ) = unpack('C4Z16Nl>',$data);
359 my $hash = {
360 u1 => $u1,
361 u2 => $u2,
362 set => ( $set_item & 0xf0 ) >> 4,
363 total => ( $set_item & 0x0f ),
364
365 type => $type,
366 content => $content,
367
368 branch => $br_lib >> 20,
369 library => $br_lib & 0x000fffff,
370
371 custom => $custom,
372 };
373
374 return $hash;
375 }
376
377 sub read_tag {
378 my ( $tag ) = @_;
379
380 confess "no tag?" unless $tag;
381
382 print "read_tag $tag\n";
383
384 my $start_block = 0;
385
386 while ( $start_block < $max_rfid_block ) {
387
388 cmd(
389 sprintf( "D6 00 0D 02 $tag %02x %02x ffff", $start_block, $read_blocks ),
390 "read $tag offset: $start_block blocks: $read_blocks",
391 "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";
392 $start_block = read_tag_data( $start_block, @_ );
393 warn "# read tag upto $start_block\n";
394 },
395 "D6 00 0F FE 00 00 05 01 $tag 941A", sub {
396 print "FIXME: tag $tag ready? (expected block read instead)\n";
397 },
398 );
399
400 }
401
402 my $security;
403
404 cmd(
405 "D6 00 0B 0A $tag 1234", "check security $tag",
406 "D6 00 0D 0A 00", sub {
407 my $rest = shift;
408 my $from_tag;
409 ( $from_tag, $security ) = ( substr($rest,0,8), substr($rest,8,1) );
410 die "security from other tag: ",as_hex( $from_tag ) if $from_tag ne str2bytes( $tag );
411 $security = as_hex( $security );
412 warn "# SECURITY $tag = $security\n";
413 }
414 );
415
416 print "TAG $tag ", dump(decode_tag( $tag ));
417 }
418
419 sub write_tag {
420 my ($tag) = @_;
421
422 my $path = "$program_path/$tag";
423
424 my $data = read_file( $path );
425 my $hex_data;
426
427 if ( $data =~ s{^hex\s+}{} ) {
428 $hex_data = $data;
429 $hex_data =~ s{\s+}{}g;
430 } else {
431
432 $data .= "\0" x ( 4 - ( length($data) % 4 ) );
433
434 my $max_len = $max_rfid_block * 4;
435
436 if ( length($data) > $max_len ) {
437 $data = substr($data,0,$max_len);
438 warn "strip content to $max_len bytes\n";
439 }
440
441 $hex_data = unpack('H*', $data);
442 }
443
444 my $len = length($hex_data) / 2;
445 # pad to block size
446 $hex_data .= '00' x ( 4 - $len % 4 );
447 my $blocks = sprintf('%02x', length($hex_data) / 4);
448
449 print "write_tag $tag = ",dump( $data ), " [$len/$blocks] == $hex_data\n";
450
451 cmd(
452 "d6 00 ff 04 $tag 00 $blocks 00 $hex_data ffff", "write $tag",
453 "d6 00 0d 04 00 $tag $blocks afb1", sub { assert() },
454 ); # foreach ( 1 .. 3 ); # XXX 3m software does this three times!
455
456 my $to = $path;
457 $to .= '.' . time();
458
459 rename $path, $to;
460 print ">> $to\n";
461
462 delete $tags_data->{$tag}; # force re-read of tag
463 }
464
465 sub secure_tag {
466 my ($tag) = @_;
467
468 my $path = "$secure_path/$tag";
469 my $data = substr(read_file( $path ),0,2);
470
471 cmd(
472 "d6 00 0c 09 $tag $data 1234", "secure $tag -> $data",
473 "d6 00 0c 09 00 $tag 1234", sub { assert() },
474 );
475
476 my $to = $path;
477 $to .= '.' . time();
478
479 rename $path, $to;
480 print ">> $to\n";
481 }
482
483 exit;
484
485 for ( 1 .. 3 ) {
486
487 # ++-->type 00-0a
488 # 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
489 # 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
490 # 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
491
492 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 $_" );
493 warn "D6 00 0D 04 00 E00401003123AA26 07 CFF1 -- ack 7 block?\n";
494
495 }
496 warn " D6 00 0F FE 00 00 05 01 E00401003123AA26 941A\n";
497
498 cmd( 'D6 00 05 FE 00 05 FA 40', "port-write scan $_" ) foreach ( 1 .. 2 );
499
500 cmd('D6 00 0C 09 E00401003123AA26 D7 3AF0', 'checkin?',
501 'D6 00 0C 09 00 E00401003123AA26 6A44 -- no?' );
502 cmd('D6 00 0C 09 E00401003123AA26 DA EB5D', 'checkout?',
503 'D6 00 0C 09 00 E00401003123AA26 6A44 -- no?' );
504
505 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',
506 'D6 00 0D 04 00 E00401003123AA26 06 DFD0 -- ack 6 blocks' ) foreach ( 1 .. 3 );
507
508 undef $port;
509 print "Port closed\n";
510
511 sub writechunk
512 {
513 my $str=shift;
514 my $count = $port->write($str);
515 my $len = length($str);
516 die "wrong write length $count != $len in ",as_hex( $str ) if $count != $len;
517 print "#> ", as_hex( $str ), "\t[$count]\n" if $debug;
518 }
519
520 sub as_hex {
521 my @out;
522 foreach my $str ( @_ ) {
523 my $hex = unpack( 'H*', $str );
524 $hex =~ s/(..)/$1 /g if length( $str ) > 2;
525 $hex =~ s/\s+$//;
526 push @out, $hex;
527 }
528 return join(' | ', @out);
529 }
530
531 sub read_bytes {
532 my ( $len, $desc ) = @_;
533 my $data = '';
534 while ( length( $data ) < $len ) {
535 my ( $c, $b ) = $port->read(1);
536 die "no bytes on port: $!" unless defined $b;
537 #warn "## got $c bytes: ", as_hex($b), "\n";
538 $data .= $b;
539 }
540 $desc ||= '?';
541 warn "#< ", as_hex($data), "\t$desc\n" if $debug;
542 return $data;
543 }
544
545 our $assert;
546
547 # my $rest = skip_assert( 3 );
548 sub skip_assert {
549 assert( 0, shift );
550 }
551
552 sub assert {
553 my ( $from, $to ) = @_;
554
555 $from ||= 0;
556 $to = length( $assert->{expect} ) if ! defined $to;
557
558 my $p = substr( $assert->{payload}, $from, $to );
559 my $e = substr( $assert->{expect}, $from, $to );
560 warn "EXPECTED ",as_hex($e), " GOT ", as_hex($p), " [$from-$to] in ",dump( $assert ), "\n" if $e ne $p;
561
562 # return the rest
563 return substr( $assert->{payload}, $to );
564 }
565
566 use Digest::CRC;
567
568 sub crcccitt {
569 my $bytes = shift;
570 my $crc = Digest::CRC->new(
571 # midified CCITT to xor with 0xffff instead of 0x0000
572 width => 16, init => 0xffff, xorout => 0xffff, refout => 0, poly => 0x1021, refin => 0,
573 ) or die $!;
574 $crc->add( $bytes );
575 pack('n', $crc->digest);
576 }
577
578 # my $checksum = checksum( $bytes );
579 # my $checksum = checksum( $bytes, $original_checksum );
580 sub checksum {
581 my ( $bytes, $checksum ) = @_;
582
583 my $len = ord(substr($bytes,2,1));
584 my $len_real = length($bytes) - 1;
585
586 if ( $len_real != $len ) {
587 print "length wrong: $len_real != $len\n";
588 $bytes = substr($bytes,0,2) . chr($len_real) . substr($bytes,3);
589 }
590
591 my $xor = crcccitt( substr($bytes,1) ); # skip D6
592 warn "## checksum ",dump( $bytes, $xor, $checksum ) if $debug;
593
594 if ( defined $checksum && $xor ne $checksum ) {
595 print "checksum doesn't match: ", as_hex($xor), " != ", as_hex($checksum), " data: ", as_hex($bytes), "\n";
596 return $bytes . $xor;
597 }
598 return $bytes . $checksum;
599 }
600
601 our $dispatch;
602
603 sub readchunk {
604 # sleep 1; # FIXME remove
605
606 # read header of packet
607 my $header = read_bytes( 2, 'header' );
608 my $length = read_bytes( 1, 'length' );
609 my $len = ord($length);
610 my $data = read_bytes( $len, 'data' );
611
612 my $payload = substr( $data, 0, -2 );
613 my $payload_len = length($data);
614 warn "## payload too short $payload_len != $len\n" if $payload_len != $len;
615
616 my $checksum = substr( $data, -2, 2 );
617 checksum( $header . $length . $payload , $checksum );
618
619 print "<< ",as_hex( $header ), " [$len] ", as_hex( $payload ), " | sum: ",as_hex($checksum),"\n" if $verbose;
620
621 $assert->{len} = $len;
622 $assert->{payload} = $payload;
623
624 my $full = $header . $length . $data; # full
625 # find longest match for incomming data
626 my ($to) = grep {
627 my $match = substr($payload,0,length($_));
628 m/^\Q$match\E/
629 } sort { length($a) <=> length($b) } keys %$dispatch;
630 warn "?? payload dispatch to ",dump( $payload, $dispatch, $to ) if $debug;
631
632 if ( defined $to ) {
633 my $rest = substr( $payload, length($to) ) if length($to) < length($payload);
634 warn "## DISPATCH payload to with rest", dump( $payload, $to, $rest ) if $debug;
635 $dispatch->{ $to }->( $rest );
636 } else {
637 print "NO DISPATCH for ",dump( $full ),"\n";
638 }
639
640 return $data;
641 }
642
643 sub str2bytes {
644 my $str = shift || confess "no str?";
645 my $b = $str;
646 $b =~ s/\s+//g;
647 $b =~ s/(..)/\\x$1/g;
648 $b = "\"$b\"";
649 my $bytes = eval $b;
650 die $@ if $@;
651 warn "## str2bytes( $str ) => $b => ",as_hex($bytes) if $debug;
652 return $bytes;
653 }
654
655 sub cmd {
656 my $cmd = shift || confess "no cmd?";
657 my $cmd_desc = shift || confess "no description?";
658 my @expect = @_;
659
660 my $bytes = str2bytes( $cmd );
661
662 # fix checksum if needed
663 $bytes = checksum( substr( $bytes, 0, -2 ), substr( $bytes, -2, 2 ) );
664
665 warn ">> ", as_hex( $bytes ), "\t## $cmd_desc\n" if $verbose;
666 $assert->{send} = $cmd;
667 writechunk( $bytes );
668
669 while ( @expect ) {
670 my $pattern = str2bytes( shift @expect ) || confess "no pattern?";
671 my $coderef = shift @expect || confess "no coderef?";
672 confess "not coderef" unless ref $coderef eq 'CODE';
673
674 next if defined $dispatch->{ $pattern };
675
676 $dispatch->{ substr($pattern,3) } = $coderef;
677 warn "++ dispatch ", as_hex($pattern) ,dump( $dispatch ) if $debug;
678 }
679
680 readchunk;
681 }
682

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.26