/[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 43 - (show annotations)
Tue Jun 23 12:19:30 2009 UTC (14 years, 10 months ago) by dpavlin
File MIME type: text/plain
File size: 17385 byte(s)
- implement simple local http server
- scan_for_tags added
- decode_tag return hash of tag data

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

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.26