first draft of mon-functioning SIP2 selfcheck
authorDobrica Pavlinusic <dpavlin@rot13.org>
Fri, 22 Aug 2014 11:25:23 +0000 (13:25 +0200)
committerDobrica Pavlinusic <dpavlin@rot13.org>
Fri, 22 Aug 2014 11:25:23 +0000 (13:25 +0200)
examples/selfcheck.html [new file with mode: 0644]
examples/selfcheck.js [new file with mode: 0644]
scripts/RFID-JSONP-server.pl

diff --git a/examples/selfcheck.html b/examples/selfcheck.html
new file mode 100644 (file)
index 0000000..df0cd70
--- /dev/null
@@ -0,0 +1,164 @@
+<html>
+<head>
+<meta charset="utf-8"></meta>
+<title>RFID SIP2 Selfcheck</title>
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+<style type="text/css">
+.status {
+       background: #ff8;
+}
+
+.da {
+       background: #fcc;
+}
+
+.d7 {
+       background: #cfc;
+}
+
+label[for=pull-reader] {
+       position: absolute;
+       top: 1em;
+       right: 1em;
+       background: #eee;
+}
+
+,page {
+       font-size: 300%;
+       border: 10px solid #ccc;
+
+}
+
+#debug {
+       position: fixed;
+       width: 20em;
+       top: 0;
+       right: 0;
+}
+
+input[type=button] {
+       padding-top: 2em;
+       display: block;
+       width: 100%;
+       height: 10em;
+}
+
+</style>
+<script type="text/javascript" src="/examples/selfcheck.js"></script>
+</head>
+<body>
+
+<div id="debug">
+
+<label for=pull-reader>
+<input id=pull-reader type=checkbox checked=1>
+active
+</label>
+
+<div id="tags">
+RFID reader not found or driver program not started.
+</div>
+
+</div>
+
+<h1>Knjižnica Filozofskog fakulteta</h1>
+
+<div class="page" id="start">
+
+<!--
+Put borrower card on a reader to borrow books
+-->
+
+Ako želite posuditi knjigu, najprije stavite iskaznicu na za to previđeno mjesto.
+
+<hr>
+
+<!--
+Select checkin button to return books.
+-->
+
+Ako želite vratiti knjigu, pristisnite ovaj gumb.
+
+<input name=checkin type=button value="Povrat" onClick="change_page('checkin')">
+</div>
+
+<div class="page" id="borrower_check">
+<!--
+Fetching borrower
+-->
+Provjera korisničnog računa...
+<p>
+Možete ukloniti iskaznicu.
+<span class="borrower_number"></span>
+</div>
+
+<div class="page" id="borrower_info">
+<!--
+Borrower:
+-->
+<h2>Informacije o članu</h2>
+<div class="borrower_name"></div>
+<!--
+<div class="borrower_email"></div>
+-->
+<div class="borrower_number"></div>
+
+<h2>Dokumenti</h2>
+
+<ul>
+<li>Posuđeno: <span class="hold_items">0</span>
+<li>Kašnjenje: <span class="overdue_items">0</span>
+<!-- Koha doesn't support charged items or fine items
+<li>Trenutno rezervacija: 0
+<li>Spremno za podizanje: 0
+-->
+</ul>
+
+<h2>Dugovanje</h2>
+
+Ostatak: 0,00 kn
+
+<h2>Poruke</h2>
+
+Greeting from Koha.
+
+<input type="button" value="OK" onClick="change_page(circulation_type)">
+
+
+</div>
+
+<div class="page" id="circulation">
+<!--
+Put books to borrow on reader pad.
+-->
+Postavite knjige na predviđeno mjesto ispred monitora.
+
+<div class="checkout">Posuđeno:</div>
+<div class="checkin">Vraćeno:</div>
+
+<ul id="books">
+</ul>
+
+Dokumenata: 0
+
+<div style="display: block">
+<input type="button" value="Završi" onClick="change_page('end')">
+<input class="checkout" type="button" value="Informacije o članu" onClick="change_page('borrower_info')">
+</div>
+
+</div>
+
+<div class="page" id="end">
+
+Hvala što ste koristili selfcheck sustav.
+
+</div>
+
+<div class="page" id="error">
+
+Greška. Odnesite knjige na pult.
+
+</div>
+
+</body>
+</html>
diff --git a/examples/selfcheck.js b/examples/selfcheck.js
new file mode 100644 (file)
index 0000000..9e72d58
--- /dev/null
@@ -0,0 +1,196 @@
+
+// mock console
+if(!window.console) {
+       window.console = new function() {
+               this.info = function(str) {};
+               this.error = function(str) {};
+               this.debug = function(str) {};
+       };
+}
+
+var state;
+var scan_timeout;
+
+function change_page(new_state) {
+       if ( state != new_state ) {
+
+               if ( new_state == 'checkin' ) {
+                       new_state = 'circulation'; // page has different name
+                       $('.checkout').hide();
+                       $('.checkin').show();
+               } else if ( new_state == 'checkout' ) {
+                       new_state = 'circulation'; // page has different name
+                       $('.checkout').show();
+                       $('.checkin').hide();
+               }
+
+               state = new_state;
+
+               $('.page').each( function(i,el) {
+                       if ( el.id != new_state ) {
+                               $(el).hide();
+                       } else {
+                               $(el).show();
+                       }
+               });
+               console.info('change_page', state);
+
+               if ( state == 'start' ) {
+                       start_scan();
+               }
+               if ( state == 'error' ) {
+                       // FIXME: implement timeout and go back to start
+               }
+       }
+}
+
+function got_visible_tags(data,textStatus) {
+       var html = 'No tags in range';
+       if ( data.tags ) {
+               html = '<ul class="tags">';
+               $.each(data.tags, function(i,tag) {
+                       console.debug( i, tag );
+                       html += '<li><tt class="' + tag.security + '">' + tag.sid;
+                       var content = tag.content
+                       if (typeof content === undefined && typeof tag.borrower !== undefined) 
+                               content = tag.borrower.cardnumber;
+
+                       var is_book = false;
+                       var is_borrower = false;
+
+                       if ( content ) {
+                               var link;
+                               if ( content.length = 10 && content.substr(0,3) == 130 ) { // book
+                                       is_book = true;
+                                       link = 'catalogue/search.pl?q=';
+                               } else if ( content.length == 12 && content.substr(0,2) == 20 ) {
+                                       is_borrower = true;
+                                       link = 'members/member.pl?member=';
+                               } else {
+                                       html += '<b>UNKNOWN TAG</b> '+content;
+                               }
+
+                               if ( link ) {
+                                       html += ' <a href="http://koha.example.com:8080/cgi-bin/koha/'
+                                               + link + content
+                                               + '" title="lookup in Koha" target="koha-lookup">' + content + '</a>';
+                                               + '</tt>';
+                               }
+
+                               console.debug( 'calling', state, content );
+                               window[state]( content ); // call function with barcode
+
+                       }
+               });
+               html += '</ul>';
+       }
+
+       var arrows = Array( 8592, 8598, 8593, 8599, 8594, 8600, 8595, 8601 );
+
+       html = '<div class=status>'
+               + textStatus
+               + ' &#' + arrows[ data.time % arrows.length ] + ';'
+               + '</div>'
+               + html
+               ;
+       $('#tags').html( html );
+       scan_timeout = window.setTimeout(function(){
+               scan_tags();
+       },200); // re-scan every 200ms
+};
+
+function scan_tags() {
+       if ( $('input#pull-reader').attr('checked') ) {
+               console.info('scan_tags');
+               $.getJSON("/scan?callback=?", got_visible_tags);
+       }
+}
+
+function start_scan() {
+       $('input#pull-reader').attr('checked', true);
+       scan_tags();
+}
+
+function stop_scan() {
+       $('input#pull-reader').attr('checked', '');
+}
+
+$(document).ready(function() {
+               $('input#pull-reader').click( function() {
+                       scan_tags();
+               });
+
+               $('div#tags').click( function() {
+                       $('input#pull-reader').attr('checked', false);
+               } );
+
+               change_page('start');
+});
+
+function fill_in( where, value ) {
+       $('.'+where).each(function(i, el) {
+               $(el).html(value);
+       });
+
+}
+
+/* Selfcheck state actions */
+
+var borrower_cardnumber;
+var circulation_type;
+var book_barcodes;
+
+function start( cardnumber ) {
+
+       if ( cardnumber.length != 12 || cardnumber.substr(0,2) != "20" ) {
+               console.error(cardnumber, ' is not borrower card');
+               return;
+       }
+
+       borrower_cardnumber = cardnumber;
+       circulation_type = 'checkout';
+       book_barcodes = {};
+
+       change_page('borrower_check');
+}
+
+function borrower_check() {
+
+       stop_scan();
+
+       fill_in( 'borrower_number', borrower_cardnumber );
+
+       $.getJSON('/sip2/patron_info/'+borrower_cardnumber)
+       .done( function( data ) {
+               console.info('patron', data);
+               fill_in( 'borrower_name', data['AE'] );
+               fill_in( 'borrower_email', data['BE'] );
+               fill_in( 'hold_items',    data['fixed'].substr( 2 + 14 + 3 + 18 + ( 0 * 4 ), 4 ) ) * 1;
+               fill_in( 'overdue_items', data['fixed'].substr( 2 + 14 + 3 + 18 + ( 1 * 4 ), 4 ) ) * 1;
+               change_page('borrower_info');
+       }).fail( function(data) {
+               change_page('error');
+       });
+}
+
+function borrower_info() {
+       // nop
+}
+
+function circulation( barcode ) {
+       if ( barcode
+                       && barcode.length == 10
+                       && barcode.substr(0,3) == 130
+                       && ! book_barcodes[barcode]
+       ) { // book, not seen yet
+               $.getJSON('/sip2/'+circulation_type+'/'+borrower_cardnumber+'/'+barcode , function( data ) {
+                       console.info( circulation_type, data );
+                       $('ul#books').append('<li>' + data['AJ'] + ' <small>' + data['AF'] + '</small></li>');
+                       book_barcodes[ barcode ] = 1;
+                       console.debug( book_barcodes );
+               }).fail( function() {
+                       change_page('error');
+               });
+       }
+}
+
index 1c93c2d..298ff17 100755 (executable)
@@ -19,6 +19,8 @@ use JSON::XS;
 use IO::Socket::INET;
 use LWP::UserAgent;
 use URI;
+use POSIX qw(strftime);
+use Encode;
 
 my $debug = 1;
 my $listen = '127.0.0.1:9000';
@@ -27,6 +29,14 @@ my $reader;
 my $koha_url = 'http://ffzg.koha-dev.rot13.org:8080';
 # internal URL so we can find local address of machine and vmware NAT
 my $rfid_url = 'http://rfid.koha-dev.vbz.ffzg.hr';
+my $sip2 = {
+       server   => '10.60.0.11:6002', # must be IP!
+#      user     => 'sip2-user',
+#      password => 'sip2-passwd',
+       user     => 'sip2user',
+       password => 'viva2koha',
+       loc      => 'FFZG',
+};
 
 use Getopt::Long;
 
@@ -62,6 +72,54 @@ sub rfid_borrower {
        }
 }
 
+
+sub sip2_message {
+       my $send = shift;
+
+       my $sock = $sip2->{sock} || die "no sip2 socket";
+
+       local $/ = "\r";
+
+       $send .= "\r" unless $send =~ m/\r$/;
+       warn "SIP2 >>>> ",dump($send), "\n";
+       print $sock $send;
+       $sock->flush;
+       
+       my $expect = substr($send,0,2) | 0x01;
+
+       my $in = <$sock>;
+       $in =~ s/^\n//;
+       warn "SIP2 <<<< ",dump($in), "\n";
+
+       die "expected $expect" unless substr($in,0,2) != $expect;
+
+       $in =~ s/\r$//;
+
+       my $hash;
+       if ( $in =~ s/^([0-9\s]+)// ) {
+               $hash->{fixed} = $1;
+       }
+       foreach ( split(/\|/, $in ) ) {
+               my ( $f, $v ) = ( $1, $2 ) if m/([A-Z]{2})(.+)/;
+               $hash->{$f} = decode('utf-8',$v);
+       }
+
+       warn "# sip2 hash response ",dump($hash);
+
+       return $hash;
+}
+
+if ( my $server = $sip2->{server} ) {
+       my $sock = $sip2->{sock} = IO::Socket::INET->new( $server ) || die "can't connect to $server: $!";
+       warn "SIP2 server ", $sock->peerhost, ":", $sock->peerport, "\n";
+
+       # login
+       if ( sip2_message("9300CN$sip2->{user}|CO$sip2->{password}|")->{fixed} !~ m/^941/ ) {
+               die "SIP2 login failed";
+       }
+
+}
+
 use lib 'lib';
 use Biblio::RFID::RFID501;
 use Biblio::RFID::Reader;
@@ -117,7 +175,8 @@ sub http_server {
                                my $size = -s $path;
                                warn "static $path $size bytes\n";
                                my $content_type = 'text/plain';
-                               $content_type = 'application/javascript' if $path =~ /\.js/;
+                               $content_type = 'application/javascript' if $path =~ /\.js$/;
+                               $content_type = 'text/html' if $path =~ /\.html$/;
                                print $client "HTTP/1.0 200 OK\r\nContent-Type: $content_type\r\nContent-Length: $size\r\n\r\n";
                                {
                                        local $/ = undef;
@@ -189,6 +248,36 @@ sub http_server {
                                        print $client "HTTP/1.0 $status $method\r\nLocation: $server_url\r\n\r\n";
                                }
 
+                       } elsif ( $method =~ m{/sip2/(\w+)/(.+)} ) {
+                               my ( $method, $args ) = ( $1, $2 );
+                               warn "SIP2: $method [$args]";
+
+                               my $ts = strftime('%Y%m%d    %H%M%S', localtime());
+                               my $loc      = $sip2->{loc} || die "missing sip->{loc}";
+                               my $password = $sip2->{password} || die "missing sip->{password}";
+
+                               my $hash;
+
+                               if ( $method eq 'patron_info' ) {
+                                       my $patron = $args;
+                                       $hash = sip2_message("63000${ts}          AO$loc|AA$patron|AC$password|");
+
+                               } elsif ( $method eq 'checkout' ) {
+                                       my ($patron,$barcode) = split(/\//, $args, 2);
+                                       $hash = sip2_message("11YN${ts}                  AO$loc|AA$patron|AB$barcode|AC$password|BON|BIN|");
+
+                               } elsif ( $method eq 'checkin' ) {
+                                       my $barcode = $args;
+                                       $hash = sip2_message("09N${ts}${ts}AP|AO${loc}|AB$barcode|AC|BIN|");
+                               } else {
+                                       print $client "HTTP/1.0 500 $method not implemented\r\n\r\n";
+                               }
+
+                               if ( $hash ) {
+                                       print $client "HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n",
+                                               encode_json( $hash );
+                               }
+
                        } else {
                                print $client "HTTP/1.0 404 Unkown method\r\n\r\n";
                        }