File Coverage

lib/Biblio/RFID/Reader/3M810.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Biblio::RFID::Reader::3M810;
2              
3             =head1 NAME
4              
5             Biblio::RFID::Reader::3M810 - support for 3M 810 RFID reader
6              
7             =head1 DESCRIPTION
8              
9             This module uses L over USB/serial adapter
10             with 3M 810 RFID reader, often used in library applications.
11              
12             This is most mature implementation which supports full API defined
13             in L. This include scanning for all tags in reader
14             range, reading and writing of data, and AFI security manipulation.
15              
16             This implementation is developed using Portmon on Windows to capture serial traffic
17             L
18              
19             Checksum for this reader is developed using help from C
20             L
21              
22             More inforation about process of reverse engeeniring protocol with
23             this reader is available at L
24              
25             =cut
26              
27 3     3   67753 use warnings;
  3         18  
  3         111  
28 3     3   17 use strict;
  3         7  
  3         105  
29              
30 3     3   16 use base 'Biblio::RFID::Reader::Serial';
  3         7  
  3         1011  
31 3     3   683 use Biblio::RFID;
  3         7  
  3         382  
32              
33 3     3   17 use Data::Dump qw(dump);
  3         5  
  3         124  
34 3     3   18 use Carp qw(confess);
  3         15  
  3         134  
35 3     3   1966 use Time::HiRes;
  3         4610  
  3         21  
36 3     3   1759 use Digest::CRC;
  0            
  0            
37              
38             sub serial_settings {{
39             baudrate => "19200",
40             databits => "8",
41             parity => "none",
42             stopbits => "1",
43             handshake => "none",
44             }}
45              
46             sub assert;
47              
48             my $port;
49             sub init {
50             my $self = shift;
51             $port = $self->port;
52              
53             # disable timeouts
54             $port->read_char_time(0);
55             $port->read_const_time(0);
56              
57             # drain on startup
58             my ( $count, $str ) = $port->read(3);
59             if ( $count ) {
60             my $data = $port->read( ord(substr($str,2,1)) );
61             warn "drain ",as_hex( $str, $data ),"\n";
62             }
63              
64             $port->read_char_time(100); # 0.1 s char timeout
65             $port->read_const_time(500); # 0.5 s read timeout
66              
67             $port->write( hex2bytes( 'D5 00 05 04 00 11 8C66' ) );
68             # hw-version expect: 'D5 00 09 04 00 11 0A 05 00 02 7250'
69             my $data = $port->read( 12 );
70             return unless $data;
71              
72             warn "# probe response: ",as_hex($data);
73             if ( my $rest = assert $data => 'D5 00 09 04 00 11' ) {
74             my $hw_ver = join('.', unpack('CCCC', $rest));
75             warn "# 3M 810 hardware version $hw_ver\n";
76              
77             cmd(
78             '13 04 01 00 02 00 03 00 04 00','FIXME: stats? rf-on?', sub { assert(shift,
79             '13 00 02 01 01 03 02 02 03 00'
80             )});
81              
82             return $hw_ver;
83             }
84              
85             return;
86             }
87              
88             sub checksum {
89             my $bytes = shift;
90             my $crc = Digest::CRC->new(
91             # midified CCITT to xor with 0xffff instead of 0x0000
92             width => 16, init => 0xffff, xorout => 0xffff, refout => 0, poly => 0x1021, refin => 0,
93             ) or die $!;
94             $crc->add( $bytes );
95             pack('n', $crc->digest);
96             }
97              
98             sub cmd {
99             my ( $hex, $description, $coderef ) = @_;
100             my $bytes = hex2bytes($hex);
101             if ( substr($bytes,0,1) !~ /(\xD5|\xD6)/ ) {
102             my $len = pack( 'n', length( $bytes ) + 2 );
103             $bytes = $len . $bytes;
104             my $checksum = checksum($bytes);
105             $bytes = "\xD6" . $bytes . $checksum;
106             }
107              
108             warn ">> ", as_hex( $bytes ), "\t\t[$description]\n" if $debug;
109             $port->write( $bytes );
110              
111             my $r_len = $port->read(3);
112              
113             while ( length($r_len) < 3 ) {
114             $r_len = $port->read( 3 - length($r_len) );
115             }
116              
117             my $len = ord( substr($r_len,2,1) );
118             my $data = $port->read( $len );
119              
120             warn "<< ", as_hex($r_len,$data),
121             ' | ',
122             substr($data,-2,2) eq checksum(substr($r_len,1).substr($data,0,-2)) ? 'OK' : 'ERROR',
123             " $len bytes\n" if $debug;
124              
125              
126             $coderef->( $data ) if $coderef;
127              
128             }
129              
130             sub assert {
131             my ( $got, $expected ) = @_;
132             $expected = hex2bytes($expected);
133              
134             my $len = length($got);
135             $len = length($expected) if length $expected < $len;
136              
137             confess "got ", as_hex($got), " expected ", as_hex($expected)
138             unless substr($got,0,$len) eq substr($expected,0,$len);
139              
140             return substr($got,$len);
141             }
142              
143              
144             sub inventory {
145              
146             my @tags;
147              
148             cmd( 'FE 00 05', 'scan for tags', sub {
149             my $data = shift;
150             my $rest = assert $data => 'FE 00 00 05';
151             my $nr = ord( substr( $rest, 0, 1 ) );
152              
153             if ( ! $nr ) {
154             warn "# no tags in range\n";
155             } else {
156             my $tags = substr( $rest, 1 );
157             my $tl = length( $tags );
158             die "wrong length $tl for $nr tags: ",dump( $tags ) if $tl =! $nr * 8;
159              
160             foreach ( 0 .. $nr - 1 ) {
161             push @tags, hex_tag substr($tags, $_ * 8, 8);
162             }
163             }
164              
165             });
166              
167             warn "# tags ",dump @tags;
168             return @tags;
169             }
170              
171              
172             # 3M defaults: 8,4
173             # cards 16, stickers: 8
174             my $max_rfid_block = 8;
175             my $blocks = 8;
176              
177             sub _matched {
178             my ( $data, $hex ) = @_;
179             my $b = hex2bytes $hex;
180             my $l = length($b);
181             if ( substr($data,0,$l) eq $b ) {
182             warn "_matched $hex [$l] in ",as_hex($data) if $debug;
183             return substr($data,$l);
184             }
185             }
186              
187             sub read_blocks {
188             my $tag = shift || confess "no tag?";
189             $tag = shift if ref($tag);
190              
191             my $tag_blocks;
192             my $start = 0;
193             cmd(
194             sprintf( "02 $tag %02x %02x", $start, $blocks ) => "read_blocks $tag $start/$blocks", sub {
195             my $data = shift;
196             if ( my $rest = _matched $data => '02 00' ) {
197              
198             my $tag = hex_tag substr($rest,0,8);
199             my $blocks = ord(substr($rest,8,1));
200             warn "# response from $tag $blocks blocks ",as_hex substr($rest,9);
201             foreach ( 1 .. $blocks ) {
202             my $pos = ( $_ - 1 ) * 6 + 9;
203             my $nr = unpack('v', substr($rest,$pos,2));
204             my $payload = substr($rest,$pos+2,4);
205             warn "## pos $pos block $nr ",as_hex($payload), $/;
206             $tag_blocks->{$tag}->[$nr] = $payload;
207             }
208             } elsif ( $rest = _matched $data => 'FE 00 00 05 01' ) {
209             warn "FIXME ready? ",as_hex $rest;
210             } elsif ( $rest = _matched $data => '02 06' ) {
211             die "ERROR ",as_hex($rest);
212             } else {
213             die "FIXME unsuported ",as_hex($rest);
214             }
215             });
216              
217             warn "# tag_blocks ",dump($tag_blocks);
218             return $tag_blocks;
219             }
220              
221             sub write_blocks {
222             my $tag = shift;
223             $tag = shift if ref $tag;
224              
225             my $data = shift;
226             $data = join('', @$data) if ref $data eq 'ARRAY';
227              
228             warn "## write_blocks ",dump($tag,$data);
229              
230             if ( length($data) % 4 ) {
231             $data .= '\x00' x ( 4 - length($data) % 4 );
232             warn "# padded data to ",dump($data);
233             }
234              
235             my $hex_data = as_hex $data;
236             my $blocks = sprintf('%02x', length($data) / 4 );
237              
238             cmd(
239             "04 $tag 00 $blocks 00 $hex_data", "write_blocks $tag [$blocks] $hex_data", sub {
240             my $data = shift;
241             if ( my $rest = _matched $data => '04 00' ) {
242             my $tag = substr($rest,0,8);
243             my $blocks = substr($rest,8,1);
244             warn "# WRITE ",as_hex($tag), " [$blocks]\n";
245             } elsif ( $rest = _matched $data => '04 06' ) {
246             die "ERROR ",as_hex($rest);
247             } else {
248             die "UNSUPPORTED";
249             }
250             }
251             );
252              
253             }
254              
255             sub read_afi {
256             my $tag = shift;
257             $tag = shift if ref $tag;
258              
259             my $afi;
260              
261             cmd(
262             "0A $tag", "read_afi $tag", sub {
263             my $data = shift;
264              
265             if ( my $rest = _matched $data => '0A 00' ) {
266              
267             my $tag = substr($rest,0,8);
268             $afi = substr($rest,8,1);
269              
270             warn "# SECURITY ", hex_tag($tag), " AFI: ", as_hex($afi);
271              
272             } elsif ( $rest = _matched $data => '0A 06' ) {
273             die "ERROR reading security from $tag ", as_hex($data);
274             } else {
275             die "IGNORED ",as_hex($data);
276             }
277             });
278             warn "## read_afi ",dump($tag, $afi);
279             return $afi;
280             }
281              
282             sub write_afi {
283             my $tag = shift;
284             $tag = shift if ref $tag;
285             my $afi = shift || die "no afi?";
286              
287             $afi = as_hex $afi;
288              
289             cmd(
290             "09 $tag $afi", "write_afi $tag $afi", sub {
291             my $data = shift;
292              
293             if ( my $rest = _matched $data => '09 00' ) {
294             my $tag_back = hex_tag substr($rest,0,8);
295             die "write_afi got $tag_back expected $tag" if $tag_back ne $tag;
296             warn "# SECURITY ", hex_tag($tag), " AFI: ", as_hex($afi);
297             } elsif ( $rest = _matched $data => '0A 06' ) {
298             die "ERROR writing AFI to $tag ", as_hex($data);
299             } else {
300             die "IGNORED ",as_hex($data);
301             }
302             });
303             warn "## write_afi ", dump( $tag, $afi );
304             return $afi;
305             }
306              
307             1
308              
309             __END__