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

Revision 3542, 12.0 kB (checked in by jerome, 14 years ago)

It mostly works now with a real Cartadis TCRS (Serial #68169). Time to
put some additional credits onto my card, otherwise I won't be able to
test for long :-)
TODO : improve robustness, and implement the few missing calls.

  • 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
25
26import serial # On Debian/Ubuntu : apt-get install python-serial
27
28# Some constants : names are mine, not Cartadis.
29#
30# Write errors
31NOERROR = 0
32ERRWRITEERROR = -1
33ERRNOCARD = -2
34ERRCARDBLOCKED = -3
35ERRUNKNOWNCARD = -4
36ERRINVALID = -5
37ERRMAXTRANSACTION = -6
38ERRVALUETOOHIGH = -7
39ERRGROUPNOTALLOWED = -8
40ERRWRITEBEFOREREAD = -9
41ERRREADBEFOREWRITE = -10
42ERRCOMPARISON = -11
43#
44# Read errors
45ERRREADERROR = -1
46#
47# Other errors
48ERRILLEGALGROUP = -1
49ERRNOTADMIN = -9
50ERRLISTFULL = -10
51ERRADMINNOTALLOWED = -11
52
53# Sensor
54SENSORNOCARD=0     # No card present
55SENSORUNKNOWN1=1   # Partially inside the TCRS
56SENSORCARDINSIDE=2 # Card is inside the TCRS
57SENSORUNKNOWN3=3   # Partially inside the TCRS
58
59class CartadisTCRS :
60    """A class to manage Cartadis TCRS vending card readers.
61
62       Documentation was found in a Cartadis TCRS reader's paper manual.
63
64       Cartadis is a registered trademark from Copie Monnaie France (C.M.F.)
65    """
66    def __init__(self, device, timeout=1.0, debug=False) :
67        """Initializes the connection to the TCRS."""
68        self.device = device
69        self.timeout = timeout
70        self.debug = debug
71
72        self.lastcommand = None
73        self.sol = chr(13) + chr(10) # start of line (begins each answer)
74        self.sollen = len(self.sol)
75        self.prompt = chr(13) + chr(10) + '$' # the prompt
76        self.promptlen = len(self.prompt)
77        self.eoc = chr(13) # end of command
78
79        # Each Cartadis vending card contain the following informations :
80        self.cardcontent = { "group" : None, # the card can only be read on a TCRS for which this group number was specifically allowed.
81                             "value" : None, # the number of credits on the card.
82                             "department" : None, # these two fields can be used to identify the owner of the card
83                             "account" : None,
84                             "trnum" : None  # Transaction number : Max 3000 for plastic cars, else 500.
85                           }
86
87        # opens the connection to the TCRS
88        self.tcrs = serial.Serial(device,
89                                  baudrate=9600,
90                                  bytesize=serial.EIGHTBITS,
91                                  parity=serial.PARITY_NONE,
92                                  stopbits=serial.STOPBITS_ONE,
93                                  xonxoff=False,
94                                  rtscts=True,
95                                  timeout=timeout)
96
97        # cleans up any data waiting to be read or written
98        try :
99            self.tcrs.flushInput()
100            self.tcrs.flushOutput()
101            self.tcrs.read(1) # Skips the first $ prompt
102        except serial.serialutil.SerialException, msg :
103            self.logError(msg)
104            self.close()
105        else :
106            # Identifies the terminal
107            self.versionNumber = self.version()
108            self.serialNumber = self.serial()
109            self.logDebug("%s TCRS detected on device %s with serial number %s" \
110                              % (self.versionNumber,
111                                 self.device,
112                                 self.serialNumber))
113
114    def __del__(self) :
115        """Ensures the serial link is closed on deletion."""
116        self.close()
117
118    def close(self) :
119        """Closes the serial link if it is open."""
120        if self.tcrs is not None :
121            self.logDebug("Closing serial link...")
122            self.tcrs.close()
123            self.tcrs = None
124            self.logDebug("Serial link closed.")
125
126    def logError(self, message) :
127        """Logs an error message."""
128        sys.stderr.write("%s\n" % message)
129        sys.stderr.flush()
130
131    def logDebug(self, message) :
132        """Logs a debug message."""
133        if self.debug :
134            self.logError(message)
135
136    def sendCommand(self, cmd, param=None) :
137        """Sends a command to the TCRS."""
138        if self.tcrs is not None :
139            if param is not None :
140                command = "%s %s%s" % (cmd, param, self.eoc)
141            else :
142                command = "%s%s" % (cmd, self.eoc)
143            self.logDebug("Sending %s to TCRS" % repr(command))
144            self.tcrs.write(command)
145            self.tcrs.flush()
146            self.lastcommand = command
147            #
148            # IMPORTANT : the following code doesn't work because currently
149            # PySerial doesn't allow an EOL marker to be several chars long.
150            # I've just sent a patch for this to PySerial's author, and we'll
151            # see what happens. If not accepted, I'll write it another way.
152            answer = self.tcrs.readline(eol=self.prompt)
153            self.logDebug("TCRS answered %s" % repr(answer))
154            if answer.startswith(command) :
155                answer = answer[len(command):]
156            if answer.startswith(self.sol) and answer.endswith(self.prompt) :
157                return answer[self.sollen:-self.promptlen]
158            else :
159                if answer != self.sol :
160                    self.logError("Unknown answer %s" % repr(answer))
161                return None
162        else :
163            self.logError("Device %s is not open" % self.device)
164
165    def help(self) :
166        """Returns the list of commands supported by the TCRS."""
167        return self.sendCommand("help")
168
169    def version(self) :
170        """Returns the TCRS' version string."""
171        return self.sendCommand("version")
172
173    def serial(self) :
174        """Returns the TCRS' serial number.'"""
175        return self.sendCommand("serial")
176
177    def read(self) :
178        """Reads the card's content to the TCRS. Returns the type of card or an error value."""
179        return int(self.sendCommand("read") or -1)
180
181    def write(self) :
182        """Writes the TCRS values to the card. Returns 0 or error value."""
183        return int(self.sendCommand("write"))
184
185    def sensor(self) :
186        """Returns 0 if there's no card in TCRS, else 1, 2 or 3."""
187        return int(self.sendCommand("sensor"))
188
189    def eject(self) :
190        """Ejects the card from the TCRS."""
191        return self.sendCommand("eject")
192
193    def trnum(self) :
194        """Returns the number of transactions made with this card."""
195        return int(self.sendCommand("trnum"))
196
197    def value(self, value=None) :
198        """Returns the last value read, or sets the new value of the card, but doesn't write it to the card yet."""
199        if value is None :
200            return int(self.sendCommand("value"))
201        else :
202            return self.sendCommand("value", str(value))
203
204    def account(self, account=None) :
205        """Returns the last account number read, or sets the account number, but doesn't write it to the card yet.'"""
206        if account is None :
207            return int(self.sendCommand("account"))
208        else :
209            return self.sendCommand("account", str(account))
210
211    def department(self, department=None) :
212        """Returns the last department number read, or sets the department number, but doesn't write it to the card yet.'"""
213        if department is None :
214            return int(self.sendCommand("department"))
215        else :
216            return self.sendCommand("department", str(department))
217
218    def group(self, group=None) :
219        """Returns the last group number read, or sets the group number, but doesn't write it to the card yet.'"""
220        if group is None :
221            return int(self.sendCommand("group"))
222        else :
223            return self.sendCommand("group", str(group))
224
225    def addgrp(self, group=None) :
226        """Adds the group to the list of allowed ones. If no group, the one on the admin card is used."""
227        return int(self.sendCommand("addgrp", str(group)))
228
229    def listgrp(self) :
230        """Returns the list of allowed group numbers."""
231        return [int(g) for g in self.sendCommand("listgrp").split()]
232
233    def delgrp(self, group) :
234        """Deletes the group from the list of allowed groups."""
235        return int(self.sendCommand("delgrp", str(group)))
236
237    def cardtype(self, cardtype=None) :
238        """Returns the type of card, or sets it (not clear in the doc if a write call is needed or not)."""
239        # TODO : doesn't seem to return a meaningful answer
240        if cardtype is None :
241            answer = self.sendCommand("cardtype")
242        else :
243            answer = self.sendCommand("cardtype", str(cardtype))
244        try :
245            return int(answer)
246        except ValueError :
247            self.logError("Unknown card type %s" % repr(answer))
248            return None
249
250    def display(self, text) :
251        """Displays a string of text on the TCRS' screen."""
252        return self.sendCommand("display", text)
253
254    def echo(self, echo) :
255        """Changes the type of echo for the TCRS' keyboard."""
256        raise NotImplementedError
257
258    def key(self, key) :
259        """Not really clear what it does..."""
260        raise NotImplementedError
261
262    def getstr(self) :
263        """Returns a string from keyboard or -1 if buffer is empty."""
264        raise NotImplementedError
265
266    def getkey(self) :
267        """Returns the value of the key pressed, or -1 if no key was hit."""
268        raise NotImplementedError
269
270    def prompt1(self, prompt1) :
271        """Changes the 'Introduce card' message."""
272        raise NotImplementedError
273
274    def prompt2(self, prompt2) :
275        """Changes the 'Credit:' message."""
276        raise NotImplementedError
277
278    def prompt3(self, prompt3) :
279        """Changes the text displayed after the value of the card (e.g. 'EURO')."""
280        raise NotImplementedError
281
282if __name__ == "__main__" :
283    # Minimal testing
284    tcrs = CartadisTCRS("/dev/ttyS0", debug=False)
285    try :
286        sys.stdout.write("%s TCRS detected on device %s with serial number %s\n" \
287                              % (tcrs.versionNumber,
288                                 tcrs.device,
289                                 tcrs.serialNumber))
290
291
292        sys.stdout.write("This Cartadis TCRS supports the following commands :\n%s\n" % tcrs.help())
293        sys.stdout.write("Allowed groups : %s\n" % tcrs.listgrp())
294
295        import time
296        sys.stdout.write("Please insert your card into the TCRS...")
297        sys.stdout.flush()
298        while True :
299            cardpresent = tcrs.sensor()
300            tcrs.logDebug("Sensor Status : %i\n" % cardpresent)
301            if cardpresent == SENSORCARDINSIDE :
302                break
303            time.sleep(1.0)
304        sys.stdout.write("\n")
305
306        sys.stdout.write("Card read status : %s\n" % tcrs.read())
307        sys.stdout.write("Group : %s\n" % tcrs.group())
308        value = tcrs.value()
309        tcrs.display("Card has %s credits" % value)
310        sys.stdout.write("Card has %s credits\n" % value)
311        sys.stdout.write("Department : %s\n" % tcrs.department())
312        sys.stdout.write("Account : %s\n" % tcrs.account())
313        sys.stdout.write("Transaction # : %s\n" % tcrs.trnum())
314        #
315        # This block commented out because I don't have many credits for testing ;-)
316        # It seems to work anyway.
317        # Now we decrement the number of credits
318        #tcrs.value(value-1)
319        # And we flush the card's content to the card
320        #sys.stdout.write("Card write status : %s\n" % tcrs.write())
321        # Now we read it back
322        #tcrs.read()
323        #sys.stdout.write("Card now has %s credits\n" % tcrs.value())
324    finally :
325        # We always do an eject, even if card not present
326        tcrs.eject()
327        tcrs.close()
Note: See TracBrowser for help on using the browser.