root / pykocard / trunk / pykocard / cartadistcrs.py @ 3544

Revision 3544, 14.7 kB (checked in by jerome, 14 years ago)

Some untested code, for a change.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# -*- coding: utf-8 -*-
2#
3# PyKoCard
4#
5# PyKoCard : Smart Card / Vending Card managing library
6#
7# (c) 2010 Jerome Alet <alet@librelogiciel.com>
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20#
21# $Id$
22#
23
24import sys
25import time
26
27import serial # On Debian/Ubuntu : apt-get install python-serial
28
29# Some constants : names are mine, not Cartadis.
30#
31# Write errors
32NOERROR = 0
33ERRWRITEERROR = -1
34ERRNOCARD = -2
35ERRCARDBLOCKED = -3
36ERRUNKNOWNCARD = -4
37ERRINVALID = -5
38ERRMAXTRANSACTION = -6
39ERRVALUETOOHIGH = -7
40ERRGROUPNOTALLOWED = -8
41ERRWRITEBEFOREREAD = -9
42ERRREADBEFOREWRITE = -10
43ERRCOMPARISON = -11
44#
45# Read errors
46ERRREADERROR = -1
47#
48# Other errors
49ERRILLEGALGROUP = -1
50ERRNOTADMIN = -9
51ERRLISTFULL = -10
52ERRADMINNOTALLOWED = -11
53
54# Sensor
55SENSORNOCARD=0     # No card present
56SENSORUNKNOWN1=1   # Partially inside the TCRS
57SENSORCARDINSIDE=2 # Card is inside the TCRS
58SENSORUNKNOWN3=3   # Partially inside the TCRS
59
60# Waiting loop delay
61WAITDELAY=1.0 # 1 second
62
63class Terminal :
64    """Base class for all terminals."""
65    def __init__(self, device, timeout=1.0, debug=False) :
66        """Must be implemented elsewhere."""
67        raise NotImplementedError
68
69    def __del__(self) :
70        """Ensures the serial link is closed on deletion."""
71        self.close()
72
73    def logError(self, message) :
74        """Logs an error message."""
75        sys.stderr.write("%s\n" % message)
76        sys.stderr.flush()
77
78    def logDebug(self, message) :
79        """Logs a debug message."""
80        if self.debug :
81            self.logError(message)
82
83class CartadisTCRS(Terminal) :
84    """A class to manage Cartadis TCRS vending card readers.
85
86       Documentation was found in a Cartadis TCRS reader's paper manual.
87
88       Cartadis is a registered trademark from Copie Monnaie France (C.M.F.)
89    """
90    def __init__(self, device, timeout=1.0, debug=False) :
91        """Initializes the connection to the TCRS."""
92        self.device = device
93        self.timeout = timeout
94        self.debug = debug
95
96        self.debitCardTypes = (3, 5, 6, 7) # TODO : define some constants for these card types
97
98        self.lastcommand = None
99        self.shortprompt = '$'
100        self.sol = chr(13) + chr(10) # start of line (begins each answer)
101        self.sollen = len(self.sol)
102        self.prompt = chr(13) + chr(10) + self.shortprompt # the prompt
103        self.promptlen = len(self.prompt)
104        self.eoc = chr(13) # end of command
105
106        # Each Cartadis vending card contain the following informations :
107        self.cardcontent = { "group" : None, # the card can only be read on a TCRS for which this group number was specifically allowed.
108                             "value" : None, # the number of credits on the card.
109                             "department" : None, # these two fields can be used to identify the owner of the card
110                             "account" : None,
111                             "trnum" : None  # Transaction number : Max 3000 for plastic cars, else 500.
112                           }
113
114        # opens the connection to the TCRS
115        self.tcrs = serial.Serial(device,
116                                  baudrate=9600,
117                                  bytesize=serial.EIGHTBITS,
118                                  parity=serial.PARITY_NONE,
119                                  stopbits=serial.STOPBITS_ONE,
120                                  xonxoff=False,
121                                  rtscts=True,
122                                  timeout=timeout)
123
124        # cleans up any data waiting to be read or written
125        try :
126            self.tcrs.flushInput()
127            self.tcrs.flushOutput()
128        except serial.serialutil.SerialException, msg :
129            self.logError(msg)
130            self.close()
131        else :
132            # Identifies the terminal
133            self.versionNumber = self.version()
134            self.serialNumber = self.serial()
135            self.logDebug("%s TCRS detected on device %s with serial number %s" \
136                              % (self.versionNumber,
137                                 self.device,
138                                 self.serialNumber))
139
140    def _sendCommand(self, cmd, param=None) :
141        """Sends a command to the TCRS."""
142        if self.tcrs is not None :
143            if param is not None :
144                command = "%s %s%s" % (cmd, param, self.eoc)
145            else :
146                command = "%s%s" % (cmd, self.eoc)
147            self.logDebug("Sending %s to TCRS" % repr(command))
148            self.tcrs.write(command)
149            self.tcrs.flush()
150            self.lastcommand = command
151            #
152            # IMPORTANT : the following code doesn't work because currently
153            # PySerial doesn't allow an EOL marker to be several chars long.
154            # I've just sent a patch for this to PySerial's author, and we'll
155            # see what happens. If not accepted, I'll write it another way.
156            answer = self.tcrs.readline(eol=self.prompt)
157            self.logDebug("TCRS answered %s" % repr(answer))
158            if answer.startswith(self.shortprompt) :
159                answer = answer[len(self.shortprompt):]
160            if answer.startswith(command) :
161                answer = answer[len(command):]
162            if answer.startswith(self.sol) and answer.endswith(self.prompt) :
163                return answer[self.sollen:-self.promptlen]
164            else :
165                if answer and (answer != self.sol) :
166                    self.logError("Unknown answer %s" % repr(answer))
167                return None
168        else :
169            self.logError("Device %s is not open" % self.device)
170
171    # Device specific calls
172    def help(self) :
173        """Returns the list of commands supported by the TCRS."""
174        return self._sendCommand("help")
175
176    def version(self) :
177        """Returns the TCRS' version string."""
178        return self._sendCommand("version")
179
180    def serial(self) :
181        """Returns the TCRS' serial number.'"""
182        return self._sendCommand("serial")
183
184    def read(self) :
185        """Reads the card's content to the TCRS. Returns the type of card or an error value."""
186        return int(self._sendCommand("read") or -1)
187
188    def write(self) :
189        """Writes the TCRS values to the card. Returns 0 or error value."""
190        return int(self._sendCommand("write"))
191
192    def sensor(self) :
193        """Returns 0 if there's no card in TCRS, else 1, 2 or 3."""
194        return int(self._sendCommand("sensor"))
195
196    def eject(self) :
197        """Ejects the card from the TCRS."""
198        return self._sendCommand("eject")
199
200    def trnum(self) :
201        """Returns the number of transactions made with this card."""
202        return int(self._sendCommand("trnum"))
203
204    def value(self, value=None) :
205        """Returns the last value read, or sets the new value of the card, but doesn't write it to the card yet."""
206        if value is None :
207            return int(self._sendCommand("value"))
208        else :
209            return self._sendCommand("value", str(value))
210
211    def account(self, account=None) :
212        """Returns the last account number read, or sets the account number, but doesn't write it to the card yet.'"""
213        if account is None :
214            return int(self._sendCommand("account"))
215        else :
216            return self._sendCommand("account", str(account))
217
218    def department(self, department=None) :
219        """Returns the last department number read, or sets the department number, but doesn't write it to the card yet.'"""
220        if department is None :
221            return int(self._sendCommand("department"))
222        else :
223            return self._sendCommand("department", str(department))
224
225    def group(self, group=None) :
226        """Returns the last group number read, or sets the group number, but doesn't write it to the card yet.'"""
227        if group is None :
228            return int(self._sendCommand("group"))
229        else :
230            return self._sendCommand("group", str(group))
231
232    def addgrp(self, group=None) :
233        """Adds the group to the list of allowed ones. If no group, the one on the admin card is used."""
234        return int(self._sendCommand("addgrp", str(group)))
235
236    def listgrp(self) :
237        """Returns the list of allowed group numbers."""
238        return [int(g) for g in self._sendCommand("listgrp").split()]
239
240    def delgrp(self, group) :
241        """Deletes the group from the list of allowed groups."""
242        return int(self._sendCommand("delgrp", str(group)))
243
244    def cardtype(self, cardtype=None) :
245        """Returns the type of card, or sets it (not clear in the doc if a write call is needed or not)."""
246        # TODO : doesn't seem to return a meaningful answer
247        if cardtype is None :
248            answer = self._sendCommand("cardtype")
249        else :
250            answer = self._sendCommand("cardtype", str(cardtype))
251        try :
252            return int(answer)
253        except ValueError :
254            self.logError("Unknown card type %s" % repr(answer))
255            return None
256
257    def display(self, text) :
258        """Displays a string of text on the TCRS' screen."""
259        return self._sendCommand("display", text)
260
261    def echo(self, echo) :
262        """Changes the type of echo for the TCRS' keyboard."""
263        raise NotImplementedError
264
265    def key(self, key) :
266        """Not really clear what it does..."""
267        raise NotImplementedError
268
269    def getstr(self) :
270        """Returns a string from keyboard or -1 if buffer is empty."""
271        raise NotImplementedError
272
273    def getkey(self) :
274        """Returns the value of the key pressed, or -1 if no key was hit."""
275        raise NotImplementedError
276
277    def prompt1(self, prompt1) :
278        """Changes the 'Introduce card' message."""
279        raise NotImplementedError
280
281    def prompt2(self, prompt2) :
282        """Changes the 'Credit:' message."""
283        raise NotImplementedError
284
285    def prompt3(self, prompt3) :
286        """Changes the text displayed after the value of the card (e.g. 'EURO')."""
287        raise NotImplementedError
288
289    # Public API
290    def close(self) :
291        """Closes the serial link if it is open."""
292        if self.tcrs is not None :
293            self.logDebug("Closing serial link...")
294            self.tcrs.close()
295            self.tcrs = None
296            self.logDebug("Serial link closed.")
297
298    def waitForCard(self) :
299        """Waits for the card to be inserted into the terminal."""
300        while tcrs.sensor() != SENSORCARDINSIDE :
301            time.sleep(WAITDELAY)
302
303class CreditCard :
304    """A class for cards."""
305    def __init__(self, terminal) :
306        """Initializes a card present in the terminal."""
307        self.terminal = terminal
308        self.value = None
309        terminal.waitForCard()
310        if terminal.read() in terminal.debitCardTypes :
311            self.value = terminal.value()
312
313    def releaseCard(self) :
314        """Ejects the card from the terminal."""
315        result = self.terminal.eject()
316        self.value = None
317        return result
318
319    def __int__(self) :
320        """Returns the number of credits on the card as an integer."""
321        return int(self.value)
322
323    def __float__(self) :
324        """Returns the number of credits on the card as a float."""
325        return float(self.value)
326
327    def __iadd__(self, other) :
328        """Increases the number of credits on a card with 'card += amount'."""
329        newvalue = self.value + other
330        writtenvalue = self.terminal.value(newvalue)
331        if writtenvalue == newvalue :
332            if self.terminal.write() == NOERROR :
333                # TODO : should we return 'writtenvalue' or read from the card again to be sure ?
334                # Is another read() call needed before ? TODO : check this again with the real card reader.
335                self.value = self.terminal.value()
336                return self.value
337        raise ValueError, "Unable to read or write the card"
338
339    def __isub__(self, other) :
340        """Decreases the number of credits on a card with 'card -= amount'."""
341        newvalue = self.value - other
342        writtenvalue = self.terminal.value(newvalue)
343        if writtenvalue == newvalue :
344            if self.terminal.write() == NOERROR :
345                # TODO : should we return 'writtenvalue' or read from the card again to be sure ?
346                # Is another read() call needed before ? TODO : check this again with the real card reader.
347                self.value = self.terminal.value()
348                return self.value
349        raise ValueError, "Unable to read or write the card"
350
351if __name__ == "__main__" :
352    # Minimal testing
353    tcrs = CartadisTCRS("/dev/ttyS0", debug=True)
354    try :
355        sys.stdout.write("%s TCRS detected on device %s with serial number %s\n" \
356                              % (tcrs.versionNumber,
357                                 tcrs.device,
358                                 tcrs.serialNumber))
359
360
361        sys.stdout.write("This Cartadis TCRS supports the following commands :\n%s\n" % tcrs.help())
362        sys.stdout.write("Allowed groups : %s\n" % tcrs.listgrp())
363
364        sys.stdout.write("Please insert your card into the TCRS...")
365        sys.stdout.flush()
366        tcrs.waitForCard()
367        sys.stdout.write("\n")
368
369        sys.stdout.write("Card read status : %s\n" % tcrs.read())
370        sys.stdout.write("Group : %s\n" % tcrs.group())
371        value = tcrs.value()
372        tcrs.display("Card has %s credits" % value)
373        sys.stdout.write("Card has %s credits\n" % value)
374        sys.stdout.write("Department : %s\n" % tcrs.department())
375        sys.stdout.write("Account : %s\n" % tcrs.account())
376        sys.stdout.write("Transaction # : %s\n" % tcrs.trnum())
377        #
378        # This block commented out because I don't have many credits for testing ;-)
379        # It seems to work anyway.
380        # Now we decrement the number of credits
381        #tcrs.value(value-1)
382        # And we flush the card's content to the card
383        #sys.stdout.write("Card write status : %s\n" % tcrs.write())
384        # Now we read it back
385        #tcrs.read()
386        #sys.stdout.write("Card now has %s credits\n" % tcrs.value())
387        #
388        tcrs.eject()
389        #
390        # And now some higher level API
391        creditcard = CreditCard(tcrs) # Waits until card is inserted
392        creditcard += 5 # This one will fail with my terminal, but won't consume my credits :-)
393        # creditcard -= 1 # This one would work with my terminal, but would consume my credits :-)
394        creditcard.release()
395    finally :
396        tcrs.close()
Note: See TracBrowser for help on using the browser.