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