rename directories
authorDobrica Pavlinusic <dpavlin@rot13.org>
Wed, 25 Aug 2010 17:14:02 +0000 (19:14 +0200)
committerDobrica Pavlinusic <dpavlin@rot13.org>
Wed, 25 Aug 2010 17:14:12 +0000 (19:14 +0200)
1  2 
lib/Biblio/RFID/RFID501.pm
lib/Biblio/RFID/Reader/3M810.pm

index 0000000,97d3ee0..5c75088
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,178 +1,180 @@@
+ package Biblio::RFID::RFID501;
+ use warnings;
+ use strict;
+ use Data::Dump qw(dump);
+ =head1 NAME
+ Biblio::RFID::RFID501 - RFID Standard for Libraries
+ =head1 DESCRIPTION
+ This module tries to decode tag format as described in document
+   RFID 501: RFID Standards for Libraries
+ L<http://solutions.3m.com/wps/portal/3M/en_US/3MLibrarySystems/Home/Resources/CaseStudiesAndWhitePapers/RFID501/>
+ Goal is to be compatibile with existing 3M Alphanumeric tag format
+ which, as far as I know, isn't specificed anywhere. My documentation about
+ this format is available at
+ L<http://saturn.ffzg.hr/rot13/index.cgi?hitchhikers_guide_to_rfid>
+ =head1 Data model
+ =head2 3M Alphanumeric tag
+  0   04 is 00 tt   i [4 bit] = number of item in set  [1 .. i .. s]
+                    s [4 bit] = total items in set
+                    tt [8 bit] = item type
+  1   dd dd dd dd   dd [16 bytes] = barcode data
+  2   dd dd dd dd
+  3   dd dd dd dd
+  4   dd dd dd dd
+  5   bb bl ll ll   b [12 bit] = branch [unsigned]
+                    l [20 bit] = library [unsigned]
+  6   cc cc cc cc   c [32 bit] = custom signed integer
+ =head2 3M Manufacturing Blank
+  0   55 55 55 55
+  1   55 55 55 55
+  2   55 55 55 55
+  3   55 55 55 55
+  4   55 55 55 55
+  5   55 55 55 55
+  6   00 00 00 00 
+ =head2 Generic blank
+  0   00 00 00 00
+  1   00 00 00 00
+  2   00 00 00 00
+ =head1 Security
+ AFI byte on RFID tag is used for security.
+ In my case, we have RFID door which can only read AFI bytes from tag and issue
+ alarm sound or ignore it depending on value of byte.
+ =over 8 
+ =item 0xD7 214
+ secured item (door will beep)
+ =item 0xDA 218
+ unsecured (door will ignore it)
+ =back
+ =head1 METHODS
+ =head2 to_hash
+   my $hash = Biblio::RFID::Decode::RFID501->to_hash( $bytes );
+   my $hash = Biblio::RFID::Decode::RFID501->to_hash( [ 'blk1', 'blk2', ... , 'blk7' ] );
+ =head2 from_hash
+   my $blocks = Biblio::RFID::Decode::RFID->from_hash({ content => "1301234567" });
+ =head2 blank_3m
+ =head2 blank
+   my $blocks = Biblio::RFID::Decode::RFID->blank;
+ =cut
+ my $item_type = {
+       1 => 'Book',
+       6 => 'CD/CD ROM',
+       2 => 'Magazine',
+       13 => 'Book with Audio Tape',
+       9 => 'Book with CD/CD ROM',
+       0 => 'Other',
+       5 => 'Video',
+       4 => 'Audio Tape',
+       3 => 'Bound Journal',
+       8 => 'Book with Diskette',
+       7 => 'Diskette',
+ };
+ sub to_hash {
+       my ( $self, $data ) = @_;
+       return unless $data;
+       $data = join('', @$data) if ref $data eq 'ARRAY';
+       warn "## to_hash ",dump($data);
+       my ( $u1, $set_item, $u2, $type, $content, $br_lib, $custom, $zero ) = unpack('C4Z16Nl>l',$data);
+       my $hash = {
+               u1 => $u1,      # FIXME 0x04
+               set => ( $set_item & 0xf0 ) >> 4,
+               total => ( $set_item & 0x0f ),
+               u2 => $u2,      # FIXME 0x00
+               type => $type,
+               type_label => $item_type->{$type},
+               content => $content,
+               branch => $br_lib >> 20,
+               library => $br_lib & 0x000fffff,
+               custom => $custom,
+       };
+       warn "expected first byte to be 0x04, not $u1\n"   if $u1 != 4;
+       warn "expected third byte to be 0x00, not $u2\n"   if $u2 != 0;
+       warn "expected last block to be zero, not $zero\n" if $zero != 0;
+       return $hash;
+ }
+ sub from_hash {
+       my ( $self, $hash ) = @_;
+       warn "## from_hash ",dump($hash);
++      $hash->{$_} ||= 0 foreach ( qw( set total type branch library ) );
++
+       return pack('C4Z16Nl>l',
+               0x04,
+               ( $hash->{set} << 4 ) | ( $hash->{total} & 0x0f ),
+               0x00,
+               $hash->{type},
+               $hash->{content},
+               ( $hash->{branch} << 20 ) | ( $hash->{library} & 0x000fffff ),
+               $hash->{custom},
+               0x00,
+       );
+ }
+ sub blank_3m {
+       return ( "\x55" x ( 6 * 4 ) ) . ( "\x00" x 4 );
+ }
+ sub blank {
+       return "\x00" x ( 4 * 3 );
+ }
+ 1;
index 0000000,a1b21a1..9b8f7ee
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,311 +1,313 @@@
 -      my $data = $port->read( ord(substr($str,2,1)) );
 -      warn "drain ",as_hex( $str, $data ),"\n";
+ package Biblio::RFID::Reader::3M810;
+ =head1 NAME
+ Biblio::RFID::Reader::3M810 - support for 3M 810 RFID reader
+ =head1 DESCRIPTION
+ This module uses L<Biblio::RFID::Reader::Serial> over USB/serial adapter
+ with 3M 810 RFID reader, often used in library applications.
+ This is most mature implementation which supports full API defined
+ in L<Biblio::RFID::Reader::API>. This include scanning for all tags in reader
+ range, reading and writing of data, and AFI security manipulation.
+ This implementation is developed using Portmon on Windows to capture serial traffic
+ L<http://technet.microsoft.com/en-us/sysinternals/bb896644.aspx>
+ Checksum for this reader is developed using help from C<selwyn>
+ L<http://stackoverflow.com/questions/149617/how-could-i-guess-a-checksum-algorithm>
+ More inforation about process of reverse engeeniring protocol with
+ this reader is available at L<http://blog.rot13.org/rfid/>
+ =cut
+ use warnings;
+ use strict;
+ use base 'Biblio::RFID::Reader::Serial';
+ use Biblio::RFID;
+ use Data::Dump qw(dump);
+ use Carp qw(confess);
+ use Time::HiRes;
+ use Digest::CRC;
+ sub serial_settings {{
+       baudrate  => "19200",
+       databits  => "8",
+       parity    => "none",
+       stopbits  => "1",
+       handshake => "none",
+ }}
+ sub assert;
+ my $port;
+ sub init {
+       my $self = shift;
+       $port = $self->port;
+       # disable timeouts
+       $port->read_char_time(0);
+       $port->read_const_time(0);
+       # drain on startup
+       my ( $count, $str ) = $port->read(3);
++      if ( $count ) {
++              my $data = $port->read( ord(substr($str,2,1)) );
++              warn "drain ",as_hex( $str, $data ),"\n";
++      }
+       $port->read_char_time(100);      # 0.1 s char timeout
+       $port->read_const_time(500); # 0.5 s read timeout
+       $port->write( hex2bytes( 'D5 00  05   04 00 11   8C66' ) );
+       # hw-version     expect: 'D5 00  09   04 00 11   0A 05 00 02   7250'
+       my $data = $port->read( 12 );
+       return unless $data;
+       warn "# probe response: ",as_hex($data);
+       if ( my $rest = assert $data => 'D5 00  09   04 00 11' ) {
+               my $hw_ver = join('.', unpack('CCCC', $rest));
+               warn "# 3M 810 hardware version $hw_ver\n";
+               cmd(
+ '13  04 01 00 02 00 03 00 04 00','FIXME: stats? rf-on?', sub { assert(shift,
+ '13  00 02 01 01 03 02 02 03 00'
+               )});
+               return $hw_ver;
+       }
+       return;
+ }
+ sub checksum {
+       my $bytes = shift;
+       my $crc = Digest::CRC->new(
+               # midified CCITT to xor with 0xffff instead of 0x0000
+               width => 16, init => 0xffff, xorout => 0xffff, refout => 0, poly => 0x1021, refin => 0,
+       ) or die $!;
+       $crc->add( $bytes );
+       pack('n', $crc->digest);
+ }
+ sub cmd {
+       my ( $hex, $description, $coderef ) = @_;
+       my $bytes = hex2bytes($hex);
+       if ( substr($bytes,0,1) !~ /(\xD5|\xD6)/ ) {
+               my $len = pack( 'n', length( $bytes ) + 2 );
+               $bytes = $len . $bytes;
+               my $checksum = checksum($bytes);
+               $bytes = "\xD6" . $bytes . $checksum;
+       }
+       warn ">> ", as_hex( $bytes ), "\t\t[$description]\n" if $debug;
+       $port->write( $bytes );
+       my $r_len = $port->read(3);
+       while ( length($r_len) < 3 ) {
+               $r_len = $port->read( 3 - length($r_len) );
+       }
+       my $len = ord( substr($r_len,2,1) );
+       my $data = $port->read( $len );
+       warn "<< ", as_hex($r_len,$data),
+               ' | ',
+               substr($data,-2,2) eq checksum(substr($r_len,1).substr($data,0,-2)) ? 'OK' : 'ERROR',
+               " $len bytes\n" if $debug;
+       $coderef->( $data ) if $coderef;
+ }
+ sub assert {
+       my ( $got, $expected ) = @_;
+       $expected = hex2bytes($expected);
+       my $len = length($got);
+       $len = length($expected) if length $expected < $len;
+       confess "got ", as_hex($got), " expected ", as_hex($expected)
+       unless substr($got,0,$len) eq substr($expected,0,$len);
+       return substr($got,$len);
+ }
+ sub inventory {
+       my @tags;
+ cmd( 'FE  00 05', 'scan for tags', sub {
+       my $data = shift;
+       my $rest = assert $data => 'FE 00 00 05';
+       my $nr = ord( substr( $rest, 0, 1 ) );
+       if ( ! $nr ) {
+               warn "# no tags in range\n";
+       } else {
+               my $tags = substr( $rest, 1 );
+               my $tl = length( $tags );
+               die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;
+               foreach ( 0 .. $nr - 1 ) {
+                       push @tags, hex_tag substr($tags, $_ * 8, 8);
+               }
+       }
+ });
+       warn "# tags ",dump @tags;
+       return @tags;
+ }
+ # 3M defaults: 8,4
+ # cards 16, stickers: 8
+ my $max_rfid_block = 8;
+ my $blocks = 8;
+ sub _matched {
+       my ( $data, $hex ) = @_;
+       my $b = hex2bytes $hex;
+       my $l = length($b);
+       if ( substr($data,0,$l) eq $b ) {
+               warn "_matched $hex [$l] in ",as_hex($data) if $debug;
+               return substr($data,$l);
+       }
+ }
+ sub read_blocks {
+       my $tag = shift || confess "no tag?";
+       $tag = shift if ref($tag);
+       my $tag_blocks;
+       my $start = 0;
+       cmd(
+                sprintf( "02 $tag %02x %02x", $start, $blocks ) => "read_blocks $tag $start/$blocks", sub {
+                       my $data = shift;
+                       if ( my $rest = _matched $data => '02 00' ) {
+                               my $tag = hex_tag substr($rest,0,8);
+                               my $blocks = ord(substr($rest,8,1));
+                               warn "# response from $tag $blocks blocks ",as_hex substr($rest,9);
+                               foreach ( 1 .. $blocks ) {
+                                       my $pos = ( $_ - 1 ) * 6 + 9;
+                                       my $nr = unpack('v', substr($rest,$pos,2));
+                                       my $payload = substr($rest,$pos+2,4);
+                                       warn "## pos $pos block $nr ",as_hex($payload), $/;
+                                       $tag_blocks->{$tag}->[$nr] = $payload;
+                               }
+                       } elsif ( $rest = _matched $data => 'FE 00 00 05 01' ) {
+                               warn "FIXME ready? ",as_hex $rest;
+                       } elsif ( $rest = _matched $data => '02 06' ) {
+                               die "ERROR ",as_hex($rest);
+                       } else {
+                               die "FIXME unsuported ",as_hex($rest);
+                       }
+       });
+       warn "# tag_blocks ",dump($tag_blocks);
+       return $tag_blocks;
+ }
+ sub write_blocks {
+       my $tag = shift;
+       $tag = shift if ref $tag;
+       my $data = shift;
+       $data = join('', @$data) if ref $data eq 'ARRAY';
+       warn "## write_blocks ",dump($tag,$data);
+       if ( length($data) % 4 ) {
+               $data .= '\x00' x ( 4 - length($data) % 4 );
+               warn "# padded data to ",dump($data);
+       }
+       my $hex_data = as_hex $data;
+       my $blocks   = sprintf('%02x', length($data) / 4 );
+       cmd(
+               "04 $tag 00 $blocks 00 $hex_data", "write_blocks $tag [$blocks] $hex_data", sub {
+                       my $data = shift;
+                       if ( my $rest = _matched $data => '04 00' ) {
+                               my $tag = substr($rest,0,8);
+                               my $blocks = substr($rest,8,1);
+                               warn "# WRITE ",as_hex($tag), " [$blocks]\n";
+                       } elsif ( $rest = _matched $data => '04 06' ) {
+                               die "ERROR ",as_hex($rest);
+                       } else {
+                               die "UNSUPPORTED";
+                       }
+               }
+       );
+ }
+ sub read_afi {
+       my $tag = shift;
+       $tag = shift if ref $tag;
+       my $afi;
+       cmd(
+               "0A $tag", "read_afi $tag", sub {
+               my $data = shift;
+               if ( my $rest = _matched $data => '0A 00' ) {
+                       my $tag = substr($rest,0,8);
+                          $afi = substr($rest,8,1);
+                       warn "# SECURITY ", hex_tag($tag), " AFI: ", as_hex($afi);
+               } elsif ( $rest = _matched $data => '0A 06' ) {
+                       die "ERROR reading security from $tag ", as_hex($data);
+               } else {
+                       die "IGNORED ",as_hex($data);
+               }
+       });
+       warn "## read_afi ",dump($tag, $afi);
+       return $afi;
+ }
+ sub write_afi {
+       my $tag = shift;
+       $tag = shift if ref $tag;
+       my $afi = shift || die "no afi?";
+       $afi = as_hex $afi;
+       cmd(
+               "09 $tag $afi", "write_afi $tag $afi", sub {
+               my $data = shift;
+               if ( my $rest = _matched $data => '09 00' ) {
+                       my $tag_back = hex_tag substr($rest,0,8);
+                       die "write_afi got $tag_back expected $tag" if $tag_back ne $tag;
+                       warn "# SECURITY ", hex_tag($tag), " AFI: ", as_hex($afi);
+               } elsif ( $rest = _matched $data => '0A 06' ) {
+                       die "ERROR writing AFI to $tag ", as_hex($data);
+               } else {
+                       die "IGNORED ",as_hex($data);
+               }
+       });
+       warn "## write_afi ", dump( $tag, $afi );
+       return $afi;
+ }
+ 1
+ __END__
+ =head1 SEE ALSO
+ L<Biblio::RFID::Reader::API>