root / pykota / trunk / contributed / itcprint / itcxfer.py @ 3207

Revision 3207, 17.7 kB (checked in by jerome, 17 years ago)

Upgraded to the latest version sent by George Farris.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Revision
Line 
1#! /usr/bin/python
2#
3# itcxfer.py
4# (c) 2007 George Farris <farrisg@shaw.ca>     
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19#
20
21
22# This documents and provides a demo of the protocol used to maniplulate
23# the ITC Print  2015 smart card reader, http://www.itcsystems.com/smart-card-stored.html
24# The  2015 is connected to the serial port of the PC to charge for things like
25# computer time usage, pay-for-print, cash registers sales, etc.
26#
27# The following description assumes that "Host" is the PC and Reader is the 2015
28#
29# -----------------------------------------------------------------------------------------------------
30# Poll card reader for indication of card insertion
31# -----------------------------------------------------------------------------------------------------
32#   Transmit from Host
33#     Char line      : <STX><NUL><SOH><SOH><ETX><NUL><BEL><EOT>
34#     Hex translation: 0x02 0x00 0x01 0x01 0x03 0x00 0x07 0x04
35#   Receive from Reader
36#     No card inserted
37#       Char line      : <STX><NUL><SOH>@<ETX><NUL>F<EOT>
38#       Hex translation: 0x02 0x00 0x01 0x40 0x03 0x00 0x46 0x04
39#     Card Inserted
40#       Char line      : <STX><NUL><SOH>@<ETX><NUL>F<EOT>
41#       Hex translation: 0x02 0x00 0x01 0x40 0x03 0x00 0x46 0x04
42# =====================================================================================================
43
44
45# -----------------------------------------------------------------------------------------------------
46# Request current dollar(1) value stored on card
47# -----------------------------------------------------------------------------------------------------
48#   Transmit from Host
49#     Char line      : <STX><NUL><SOH>!<ETX><NUL>'<EOT>
50#     Hex translation: 0x02 0x00 0x01 0x21 0x03 0x00 0x27 0x04
51#   Receive from Reader
52#     Char line      : <STX><NUL><SOH><NUL><NUL><NUL><NUL><NUL><NUL><DLE>h<SOH><ETX><NUL><DEL><EOT>
53#     Hex translation: 0x02 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x10 0x68 0x01 0x03 0x00 0x7F 0x04
54#                                     [  Dollar value in this case 0x10 0x68 ]
55#                                     [     0x1068 = 4200 = $4.20            ]
56#                                     [______________________________________]
57# =====================================================================================================
58
59
60# -----------------------------------------------------------------------------------------------------
61# Update Reader with new dollar value - must be less than what is stored on card
62# -----------------------------------------------------------------------------------------------------
63#   Transmit from Host
64#     Char line      : <STX><NUL><SOH>$<NUL><NUL><NUL><NUL><NUL><NUL><DLE><EOT><SOH><ETX><NUL>?<FF>
65#     Hex translation: 0x02 0x00 0x01 0x24 0x00 0x00 0x00 0x00 0x00 0x00 0x10 0x04 0x01 0x03 0x00 0x3F 0x0C
66#                                          [  Dollar value in this case 0x10 0x04 ]  ________[    chkm]__________
67#                                          [     0x1004 = 4100 = $4.10            ] [ checksum add bytes 1 to 15 ]
68#                                          [______________________________________] [____________________________]
69#
70#   Receive from successful transaction from Reader
71#     Char line      : <STX><NUL><SOH><SYN><ETX><NUL><FS><BS>
72#     Hex translation: 0x02 0x00 0x01 0x16 0x03 0x00 0x1C 0x08
73# =====================================================================================================
74#0200011703001d08
75
76# -----------------------------------------------------------------------------------------------------
77# Eject card from Reader
78# -----------------------------------------------------------------------------------------------------
79#   Transmit from Host
80#     Char line      : <STX><NUL><SOH><SPACE><ETX><NUL>&<EOT>
81#     Hex translation: 0x02 0x00 0x01 0x20 0x03 0x00 0x26 0x04
82#   Receive from Reader
83#     Char line      : <STX><NUL><SOH><SYN><ETX><NUL><FS><BS>
84#     Hex translation: 0x02 0x00 0x01 0x16 0x03 0x00 0x1C 0x08
85# =====================================================================================================
86
87# (1) Currency can be set from the card reader keypad
88
89import sys, os, serial, string, binascii, time
90import gtk, gtk.glade, gobject, pg
91
92# -------------- User modifiable settings -----------------------------
93# Database server settings
94HOST = '10.31.50.3'
95PORT = 5432
96DBNAME = 'pykota'
97USER = 'pykotaadmin'
98PASS = 'secret'
99
100# Search database as you type function
101# These settings control if a database search of the username is performed
102# automatically when they reach a desired length.  This helps in a University
103# setting where all student username are the same length.
104SEARCH = True
105SEARCHLENGTH = 6
106
107# Serial port settings
108SERIAL_PORT = '/dev/ttyUSB0'
109BAUD_RATE = 9600
110
111# Set this to True or False
112# If set to True it will not update any cards
113TESTMODE = True
114
115# ------------- End of user modifiable settings -----------------------
116
117class gui:
118        def __init__(self):
119                self.cardstate = 0  # 0 not inserted, 1 inserted
120               
121                self.gladefile = "itcprint.glade" 
122                self.xml = gtk.glade.XML(self.gladefile) 
123
124                self.window = self.xml.get_widget("MainWindow")
125                self.utext = self.xml.get_widget("UsernameEntry")
126                self.cardlabel = self.xml.get_widget("CardBalanceLabel")
127                self.printlabel = self.xml.get_widget("PrintBalanceLabel")
128                self.spinbutton = self.xml.get_widget("Spinbutton")
129                self.xferbutton = self.xml.get_widget("TransferButton")
130               
131                self.spinbutton.set_range(0,0)
132                self.spinbutton.set_sensitive(False)
133                self.xferbutton.set_sensitive(False)
134                self.cardlabel.set_label('<big><b>unknown</b></big>')
135                self.printlabel.set_label('<big><b>unknown</b></big>')
136               
137                self.cardbalance = 0.0
138                self.validuser = False
139                self.addbalance = 0.0
140
141                if not TESTMODE :
142                        print "We are in test mode...."
143
144                self.db = pgsql()                                               
145                self.sc = smartcard(self.db)
146               
147                #If you wanted to pass an argument, you would use a tuple like this:
148        # dic = { "on button1_clicked" : (self.button1_clicked, arg1,arg2) }
149                dic = { "on_TransferButton_clicked" : self.xferbutton_clicked,
150                                "on_GetbalanceButton_clicked" : self.getcardbalance_clicked,
151                                "on_EjectButton_clicked" : self.ejectbutton_clicked,
152                                "on_quit_activate" : (gtk.main_quit),
153                                "on_UsernameEntry_changed" : self.username_changed,
154                                "on_Spinbutton_value_changed" : self.spinvalue_changed,
155                                "on_UsernameEntry_focus_out_event" : self.username_entered,
156                                "on_UsernameEntry_activate" : (self.username_entered, None),
157                                "on_ItcPrint_destroy" : (gtk.main_quit) }
158                               
159                self.xml.signal_autoconnect (dic)
160               
161                self.completion = gtk.EntryCompletion()
162                self.utext.set_completion(self.completion)
163                self.liststore = gtk.ListStore(gobject.TYPE_STRING)
164                self.completion.set_model(self.liststore)
165                self.completion.set_text_column(0)
166                self.completion.connect("match-selected", self.username_found)
167               
168                #self.liststore.append(['string text'])
169               
170                return
171
172        # Don't allow the transfer button to be senitive unless there is a valid value
173        def spinvalue_changed(self, widget):
174                if self.spinbutton.get_value() > 0.0:
175                        self.xferbutton.set_sensitive(True)
176                else:
177                        self.xferbutton.set_sensitive(False)
178               
179        # I might want to do username search as you type later
180        def username_changed (self, widget):
181                if SEARCH :
182                        if len(self.utext.get_text()) == SEARCHLENGTH:
183                                if not self.db.alreadyopen:
184                                        if not self.db.pgopen():
185                                                result = gtk.RESPONSE_CANCEL
186                                                dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
187                                                        gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 
188                                                        "Cannot connect or open the database.\nPlease contact technical suppport...", )
189                                                result = dlg.run()
190                                                dlg.destroy()
191                                                return
192                                if self.db.get_userslist(self.utext.get_text(), self.liststore):
193                                        pass
194                                else:
195                                        return
196                               
197                                #self.username_entered(None, None)
198       
199        def username_found(self, completion, model, iter):
200                self.username = model[iter][0], 'was selected'
201                self.utext.set_text(model[iter][0])
202                self.username_entered(self, None)
203                                       
204        def username_entered (self, widget, event):
205                uname = self.utext.get_text()
206                print "Username is ->", uname
207                # This is where we need to look up username in wbinfo
208               
209
210                if not self.db.alreadyopen:
211                        if not self.db.pgopen():
212                                result = gtk.RESPONSE_CANCEL
213                                dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
214                                        gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 
215                                        "Cannot connect or open the database.\nPlease contact technical suppport...", )
216                                result = dlg.run()
217                                dlg.destroy()
218                                return
219                       
220                if self.db.get_pykotaid(uname):
221                        self.validuser = True
222                else:
223                        result = gtk.RESPONSE_CANCEL
224                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
225                                gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 
226                                "Your username is invalid or does not exist.\nPlease try re-entering it", )
227                        result = dlg.run()
228                        dlg.destroy()
229                        return
230               
231                #self.liststore.append(['string text'])
232                       
233                balance = self.db.get_pykotabalance()
234                if balance :
235                        self.printlabel.set_label("%s%.2f%s" % ("<big><b>$",balance,"</b></big>"))
236                else:
237                        result = gtk.RESPONSE_CANCEL
238                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
239                                gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 
240                                "Cannot retrieve your printing balance.\nPlease contact technical suppport...", )
241                        result = dlg.run()
242                        dlg.destroy()
243                        self.validuser = False
244                        return
245                                       
246                if not self.db.get_pykotalifebalance():
247                        result = gtk.RESPONSE_CANCEL
248                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
249                                gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 
250                                "Cannot retrieve your life time printing balance.\nPlease contact technical suppport...", )
251                        result = dlg.run()
252                        dlg.destroy()
253                        self.validuser = False
254                        return
255                       
256                # Only set transfer button if both card balance and username valid
257                if  self.cardbalance > 0.1 and self.validuser:
258                        self.spinbutton.set_sensitive(True)
259
260                       
261        def xferbutton_clicked (self, widget):
262                print "xfer button clicked...."
263                addbalance = self.spinbutton.get_value()
264               
265                if not self.db.set_pykotabalances(addbalance):
266                        result = gtk.RESPONSE_CANCEL
267                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
268                                gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, 
269                                "An error was encountered while updating your record.\nPlease contact technical support.")
270                        result = dlg.run()
271                        dlg.destroy()
272                        return
273                       
274                self.sc.set_balance(addbalance, self.cardbalance)
275                time.sleep(3)
276                self.ejectbutton_clicked(None)
277                self.spinbutton.set_range(0,float(0))
278               
279                               
280        def getcardbalance_clicked(self, widget):
281                if self.sc.checkforcardready():
282                        self.sc.waitforcardready()
283                        self.cardbalance = self.sc.get_balance()
284                        self.cardlabel.set_label("%s%.2f%s" % ("<big><b>$",self.cardbalance,"</b></big>"))
285                        self.cardstate = 1
286                        self.source_id = gobject.timeout_add(2000, self.sc.inhibit_eject)
287                        # Only allow the spin button to go up to the value of the card
288                        self.spinbutton.set_range(0,float(self.cardbalance))
289               
290                if self.cardbalance > 0.1 and self.validuser:
291                        self.spinbutton.set_sensitive(True)
292                       
293        def ejectbutton_clicked(self, widget):
294                self.sc.eject_card()
295                self.cardlabel.set_label('<big><b>unknown</b></big>')
296                self.printlabel.set_label('<big><b>unknown</b></big>')
297                self.cardstate = 0
298                self.cardbalance = 0.0
299                self.validuser = False
300                self.utext.set_text('')
301                self.addbalance = 0.0
302                self.spinbutton.set_range(0,0)
303                self.spinbutton.set_sensitive(False)
304                self.xferbutton.set_sensitive(False)
305               
306                self.db.pgclose()
307                               
308                # Is it possible this might not be set
309                try:
310                        gobject.source_remove(self.source_id)
311                except:
312                        pass
313               
314               
315class smartcard:
316        def __init__(self, sql):
317                try:
318                        self.ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
319                except:
320                        result = gtk.RESPONSE_CANCEL
321                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, 
322                                gtk.BUTTONS_CLOSE, "Fatal error - Could not open serial port...", )
323                        result = dlg.run()
324                        dlg.destroy()
325                        exit(1)
326                       
327        # Need comms to contiune to keep card in machine.
328        # This loop keeps the card in until it stops so basically the print
329        # job can release the card after it is finished
330        def checkforcardready(self):
331                # A bit of a sleep here prevents the card dialog popping up if
332                # the card is already inserted.
333                time.sleep(1)
334                self.ser.write(binascii.a2b_hex("0200010103000704"))
335                s = self.ser.read(8)
336
337                if binascii.b2a_hex(s) == "0200014003004604":
338                        result = gtk.RESPONSE_CANCEL
339                        dlg = gtk.MessageDialog(None,gtk.DIALOG_MODAL |
340                                gtk.DIALOG_DESTROY_WITH_PARENT,gtk.MESSAGE_INFO, gtk.BUTTONS_OK_CANCEL, "Please insert your card...", )
341                        result = dlg.run()
342                        if (result==gtk.RESPONSE_OK):
343                                dlg.destroy()
344                                return 1
345                        else:
346                                dlg.destroy()
347                                return 0
348                if binascii.b2a_hex(s) == "0200016c0300721c":
349                        return 1
350                               
351        def waitforcardready(self):
352                print "  Waiting for card to be inserted..."
353                self.ser.write(binascii.a2b_hex("0200010103000704"))
354                s = self.ser.read(8)
355
356                while binascii.b2a_hex(s) == "0200014003004604":
357                        #time.sleep(2)
358                        #print binascii.b2a_hex(s)
359                        self.ser.write(binascii.a2b_hex("0200010103000704"))
360                        #print "Tx -> 0200010103000704"
361                        s = self.ser.read(8)
362                        #print "Rx -> %s" % binascii.b2a_hex(s)
363               
364                if binascii.b2a_hex(s) == "0200016c0300721c":
365                        print "  Card is inserted..."
366                        return 1
367                else:
368                        print "  Card Error..."
369                        return 0
370
371        # Get current value from card
372        def get_balance(self):
373                # TODO Test checksum
374                self.ser.write(binascii.a2b_hex("0200012103002704"))
375                s1 = self.ser.read(16)
376                print binascii.b2a_hex(s1)
377                print "  %s%.2f" % ("Card valued at -> $",float(string.atoi(binascii.b2a_hex(s1[3:11]), 16))/1000)
378                return float(string.atoi(binascii.b2a_hex(s1[3:11]), 16))/1000
379
380        def set_balance(self, subvalue, cardbalance):
381                newbalance = cardbalance - subvalue
382                a = (str(newbalance)).split('.')
383                b = a[0] + string.ljust(a[1],3,'0')
384                c = "%X" % (string.atoi(b))
385                d = string.zfill(c,16)
386                chksum = self.checksum(d) 
387                decrementvalue = "02000124" + d + "0103" + chksum + "0C"
388
389                if TESTMODE:
390                        print "Current card balance -> ", cardbalance
391                        print "Amount to subtract from card -> ", subvalue
392                        print "New card balance -> ", newbalance
393                        print "Checksum -> ", chksum
394                        print "Sent to card -> ",decrementvalue 
395                        return
396               
397                print "Sent to card -> ",decrementvalue 
398                self.ser.write(binascii.a2b_hex(decrementvalue))
399                s2 = self.ser.read(8)
400                print "Result -> ", binascii.b2a_hex(s2)
401
402        def eject_card(self):
403                print "  Ejecting card ..."
404                self.ser.write(binascii.a2b_hex("0200012003002604"))
405                s2 = self.ser.read(8)
406                #print "Rx -> %s" % binascii.b2a_hex(s2)
407
408        def inhibit_eject(self):
409                self.ser.write(binascii.a2b_hex("0200010103000704"))
410                s = self.ser.read(8)
411                return True
412
413        def checksum(self, s):
414                i = 0
415                j = int('0', 16)
416
417                while i < len(s): 
418                        j = int(s[i:i+2], 16) + j
419                        i = i+2
420                j = j + int('2B', 16)  # 2B is the header command and footer bytes previously added
421                return string.zfill(("%X" % j), 4)
422       
423        def close_port(self):   
424                self.ser.close()
425
426
427class pgsql:
428        def __init__(self):
429                self.sql = None
430                self.username = ''
431                self.pykotauid = ''
432                self.balance = 0
433                self.lifebalance = 0
434                self.alreadyopen = False
435               
436        def pgopen(self):
437                try:
438                        self.sql = pg.connect(dbname=DBNAME, host=HOST, port=PORT, user=USER, passwd=PASS)
439                        self.alreadyopen = True
440                        return True
441                except:
442                        print "Problem opening database on server " + HOST + "...."
443                        return False
444       
445        def pgclose(self):
446                self.username = ''
447                self.pykotauid = ''
448                self.balance = 0
449                self.lifebalance = 0
450                self.alreadyopen = False
451                self.sql.close()
452       
453        def get_userslist(self, uname,ls):
454                try:
455                        query = self.sql.query("SELECT username FROM users WHERE username LIKE '%s'" % (uname+'%'))
456                        users = query.getresult()
457                        print "Users are ->", users
458                        ls.clear()
459                        for i in users:
460                                ls.append([i[0]])
461                        #self.username = uname
462                        return True
463                except:
464                        #print "Username is invalid"
465                        return False
466
467                               
468        def get_pykotaid(self, uname):
469                try:
470                        query = self.sql.query("SELECT id FROM users WHERE username='%s'" % (uname))
471                        self.pykotauid = (query.dictresult()[0])['id']
472                        print "User ID is  ->", self.pykotauid
473                        self.username = uname
474                        return True
475                except:
476                        print "Username is invalid"
477                        return False
478       
479        def get_pykotabalance(self):
480                try:
481                        query = self.sql.query("SELECT balance FROM users WHERE id='%s'" % (self.pykotauid))
482                        self.balance = float((query.dictresult()[0])['balance'])
483                        return self.balance
484                except:
485                        print "balance sql error..."
486                        return None
487
488        def get_pykotalifebalance(self):
489                try:
490                        query = self.sql.query("SELECT lifetimepaid FROM users WHERE id='%s'" % (self.pykotauid))
491                        self.lifebalance = float((query.dictresult()[0])['lifetimepaid'])
492                        print "%s%.2f" % ("pykotalifebalance -> $", self.lifebalance)
493                        return True
494                except:
495                        print "lifetimepaid sql error..."
496                        return False
497               
498        def set_pykotabalances(self, addbalance):       
499                newbalance = addbalance + self.balance
500                newlifebalance = self.lifebalance + addbalance
501                try:
502                        query = self.sql.query("UPDATE users SET balance=%s, lifetimepaid=%s WHERE id='%s'" % 
503                                (newbalance, newlifebalance, self.pykotauid))
504                        return True
505                except:
506                        print "sql update error..."
507                        return False
508
509
510if __name__ == "__main__":
511        hwg = gui()
512        gtk.main()
513        print "Goodbye..."
Note: See TracBrowser for help on using the browser.