#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
# generic cash-acceptor client
# protocol see ../protocol.txt
# example server implementation see ../server/exampleServer.py
# (C) 2013 Max Gaukler <development@maxgaukler.de>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# The text of the license conditions can be read at
# <http://www.gnu.org/licenses/>.
"""
Client for accessing a cash device driver.
"""
import sys
import logging
from FabLabKasse.libs.process_helper.nonblockingProcess import nonblockingProcess
import pickle
import base64
import monotonic as monotonic_time
[docs]class PaymentDeviceClient(object):
"""
Client for accessing a cash device driver. It starts a new python process
("server") for the specified device driver. It uses non-blocking
communication and talks to the server process using stdin/stdout.
"""
def __init__(self, cmd, options):
"""
:param cmd: class name of the device driver
:param options: dictionary of options (name (required), device, ...)
that were set in config.ini as deviceN_foo=value
:type options: dict(unicode, unicode)
"""
self.stopped = False
self.canAccept_cachedResponse = None
self._reset()
self.waitingForResponse = False
if (sys.version_info.major, sys.version_info.minor) != (2, 7):
raise Exception("not running in python2.7 - please update PaymentDeviceClient to use the right python version for the payment servers")
args = ["/usr/bin/env", "python2.7", "-m", "FabLabKasse.cashPayment.server." + cmd, base64.b64encode(pickle.dumps(options))]
logging.info("starting cashPayment server: PYTHONPATH=.. " + " ".join(args))
self.process = nonblockingProcess(args, {"PYTHONPATH": ".."})
self.commandline = cmd
self.options = options
self.lastCommand = ""
def __repr__(self):
return "<PaymentDeviceClient(type=" + self.commandline + ", name=" + self.options["name"] + ")>"
def _reset(self):
"""
Initialise/Reset internal state to default values
does not check for any condition!
Called at the end of an operation to go back to idle state
"""
self.lastResponseTime = None
self.finalAmount = None
self.pollAmount = None
self.requestedAccept = None
self.lastSentAccept = None
self.requestedDispense = None
self.testDispenseAnswer = None
self.status = "idle"
self.stopped = False
[docs] def poll(self):
"""
update internal status
call this regularly
:raise: Exception if the device crashed - do not try to recover from this exception, or the result of any
following calls will be undefined
"""
if not self.process.isAlive():
raise Exception("device {0} server crashed -- check cash-{1}.log. If it crashed before writing the logfile, try launching the server yourself with the commandline from gui.log ".format(self, self.options["name"]))
if not self.waitingForResponse:
if self.status == "stop":
if self.stopped:
cmd = None
# wait until getFinalAmountAndReset() is called
else:
cmd = "STOP"
elif self.status == "dispense":
cmd = "DISPENSE {0}".format(self.requestedDispense)
elif self.status == "accept":
cmd = "ACCEPT {0}".format(self.requestedAccept)
self.lastSentAccept = self.requestedAccept
elif self.status == "acceptWait":
# alternate between polling and (if necessary) updating the maximum accepted value
if self.lastSentAccept != self.requestedAccept and self.lastCommand.startswith("POLL"):
cmd = "UPDATE-ACCEPT {0}".format(self.requestedAccept)
self.lastSentAccept = self.requestedAccept
else:
cmd = "POLL"
elif self.status == "dispenseWait":
# alternate between polling (to get intermediate value) and stopping (to see if finished)
if self.lastCommand == "POLL":
cmd = "STOP"
else:
cmd = "POLL"
elif self.status == "testDispense":
cmd = "CANPAYOUT"
elif self.status == "canAccept":
cmd = "CANACCEPT"
elif self.status == "empty":
cmd = "EMPTY"
elif self.status == "emptyWait":
cmd = "POLL"
elif self.status == "idle":
cmd = None
else:
raise Exception("unknown status")
if cmd is None:
return
print "SEND CMD: " + cmd # +"\n"
self.process.write(cmd + "\n")
self.lastCommand = cmd
self.waitingForResponse = True
self.lastResponseTime = monotonic_time.monotonic() # get monotonic time. until python 3.3 we have to use this extra module because time.monotonic() is not available in older versions.
response = self.process.readline()
if response is None and self.waitingForResponse \
and monotonic_time.monotonic() - self.lastResponseTime > 50:
raise Exception("device {0} server timeout (>50sec)".format(self))
if response is not None:
print "got response: '" + response + "'"
assert self.waitingForResponse
self.waitingForResponse = False
# strip prefix
prefix = "COMMAND ANSWER:"
assert response.startswith(prefix), "response with wrong prefix: " + response
response = response[len(prefix):]
cmd = self.lastCommand
# parse response
if cmd.startswith("DISPENSE"):
assert response == "OK"
assert self.status == "dispense"
self.status = "dispenseWait" # wait for dispense to finish
elif cmd.startswith("ACCEPT"):
assert response == "OK"
assert self.status in ["accept", "stop"]
if self.status == "accept":
self.status = "acceptWait" # wait for accept to finish
elif self.status == "stop":
pass # "quickstop": the status was changed to "stop" even before the accept mode was fully started. slowly stop accepting
elif cmd.startswith("UPDATE-ACCEPT"):
assert response == "OK"
assert self.status in ["accept", "stop"]
elif cmd == "POLL":
l = response.split(" ")
assert len(l) > 1
self.pollAmount = int(l[0])
elif cmd == "CANPAYOUT":
l = response.split(" ")
assert len(l) == 2
self.testDispenseAnswer = [int(l[0]), int(l[1])]
self.status = "idle"
elif cmd == "CANACCEPT":
assert self.status == "canAccept"
if response == "True":
self.canAccept_cachedResponse = True
elif response == "False":
self.canAccept_cachedResponse = False
else:
raise Exception("response is neither False nor True:" + response)
self.status = "idle"
elif cmd == "STOP":
assert not self.stopped
assert self.status in ["stop", "dispenseWait"]
if response == "wait":
pass # do nothing, resend STOP next time
else:
self.finalAmount = int(response)
self.stopped = True
elif cmd == "EMPTY":
assert response == "OK"
self.status = "emptyWait"
else:
raise Exception("unknown command at parsing answer")
[docs] def accept(self, maximumPayin):
"""
accept up to maximumPayin money, until stopAccepting() is called
poll() must be called before other actions are taken
"""
assert self.status == "idle"
self.status = "accept"
self.requestedAccept = maximumPayin
[docs] def updateAcceptValue(self, maximumPayin):
"""
lower the amount of money that is accepted at maximum
this can be called while accept is active
example use case:
- Two payment devices should accept 50€ in total.
- 10€ were inserted into the first device -> update the second device to a maximum of 40€.
"""
if self.requestedAccept > 0 and self.status == "accept":
self.requestedAccept = maximumPayin
[docs] def stopAccepting(self):
"""
stop accepting (does not work immediately - some payins may be possible!)
:rtype: None
"""
if self.status == "accept":
# if the last sent command is not ACCEPT, we have not sent the ACCEPT command yet. this means that poll wasn't called yet.
assert self.lastCommand.startswith("ACCEPT"), "you must call poll() after accept() first before calling stopAccepting() !"
# the answer to ACCEPT was not yet received
# instead of messing up everything, just wait for the answer and then send stop commands
self.status = "stop"
return
assert self.status == "acceptWait"
assert not self.stopped
self.status = "stop"
[docs] def dispense(self, amount):
"""
Dispense up to the requested amount of money (as much as possible)
- Wait until hasStopped() is true, then retrieve the paid out value with getFinalAmountAndReset()
- An intermediate value (as a progess report) can be retrieved with getCurrentAmount, but the operation cannot
be aborted.
- If you want to make sure that enough is available, see possibleDispense()
"""
assert self.status == "idle"
self.requestedDispense = amount
self.status = "dispense"
[docs] def possibleDispense(self):
"""
how much can be paid out?
(function may only be called while no operation is in progress, will raise Exception otherwise)
return value:
- ``None``: request in progress, please call the function again until it does not return None.
No other actions (dispense/accept/canPayout) may be called until a non-None value was returned!
Call poll() repeatedly until possibleDispense()!=None.
- [maximumAmount, remainingAmount]: This one non-None response is not cached, another call will send return None again and send a new query to the device
- maximumAmount (int): the device has enough money to pay out any amount up to maximumAmount
- remainingAmount (int): How much money could be remaining at worst, if canBePaid==True? This is usually a per-device constant.
remainingAmount will be == 0 for a small-coins dispenser that includes 1ct.
.. IMPORTANT::
it can be still possible to payout more, but not any value above maximumAmount!
For example a banknote dispenser filled with 2*10€ and 5*100€ bills will return:
``possibleDispense() == [2999, 999]`` which means "can payout any value in 0...29,99€ with an unpaid rest of <= 9,99€"
But it can still fulfill a request of exactly 500€!
:rtype: None | [int, int]
"""
if self.testDispenseAnswer != None:
# we have a cached answer
a = self.testDispenseAnswer
self.testDispenseAnswer = None
return a
if self.status == "testDispense":
# request is already sent, but answer not yet received
return None
if self.status != "idle":
raise Exception("possibleDispense cannot be used while dispensing or accepting")
self.testDispenseAnswer = None
self.status = "testDispense"
return None
[docs] def canAccept(self):
"""
does the device support accept commands?
(If this function has not returned True/False once before, it may only
be called while no operation is in progress and will raise an Exception
otherwise. )
return values and usage:
- None: please call the function again later. The answer has not yet
been received from the device.
No other actions (dispense/accept/possibleDispense) may be called until
a non-None value was returned!
call poll() repeatedly until ``canAccept() != None``
- True/False: Does (not) support accepting. (Now the answer is cached and may the function may be called again
always)
:rtype: boolean | None
"""
if self.canAccept_cachedResponse != None:
return self.canAccept_cachedResponse
if self.status == "canAccept":
return None
if self.status != "idle":
raise Exception("canAccept cannot be used for the first time while dispensing or accepting")
self.status = "canAccept"
[docs] def empty(self):
"""
start service-mode emptying
The implementation of this modes is device specific:
- If the device has
an inaccessible storage, it should move the contents to the cashbox
so that it can be taken out for counting.
- If available, manual payout buttons are enabled.
usage:
- call empty()
- sleep, do something else, whatever you want...
- call poll() at least once before the next step:
- as soon as you want to stop, call stopEmptying()
- call hasStopped() until it returns True
- then call getFinalAmountAndReset()
"""
assert self.status == "idle"
self.status = "empty"
[docs] def stopEmptying(self):
"""
end the mode that was started by empty()
usage: see empty()
"""
assert self.status != "empty", "stopEmptying() called before first poll()"
assert self.status == "emptyWait"
assert not self.stopped
self.status = "stop"
[docs] def getCurrentAmount(self):
"""how much has currently been paid in?
(value is not always up-to-date, but will not be higher than the actual value)
"""
return self.pollAmount
[docs] def hasStopped(self):
"""returns True as soon as the operation (accept/dispense) has finished"""
return self.stopped
[docs] def getFinalAmountAndReset(self):
"""call this as soon as hasStopped() is true. this returns the final amount paid in/out (negative for payout)"""
assert self.hasStopped()
r = self.finalAmount
self._reset()
return r
#==============================================================================
# old demo code that is currently not working because
# if __name__ == "__main__":
# if "--nv11-demo" in sys.argv:
# nv11_demo()
# else:
# dummy_demo()
#
#
# def nv11_demo():
# a = PaymentDeviceClient("../server/banknotenleser.py")
# a.poll()
# while a.canAccept() == None:
# print "waiting for canAccept"
# a.poll()
# time.sleep(1)
# print a.canAccept()
# a.accept(2341)
# for i in range(42):
# a.poll()
# print a.getCurrentAmount()
# time.sleep(1)
# a.stopAccepting()
# while not a.hasStopped():
# a.poll()
# time.sleep(1)
# print a.getFinalAmountAndReset()
#
# print "can dispense: ..."
# canDispense = None
# while canDispense == None:
# canDispense = a.possibleDispense()
# a.poll()
# time.sleep(1)
# print "can dispense:", canDispense
#
# a.dispense(2341)
#
# while not a.hasStopped():
# a.poll()
# print "waiting for dispense to stop, currently dispensed", a.getCurrentAmount()
# time.sleep(1)
# print "final dispensed:", a.getFinalAmountAndReset()
#
#
# def dummy_demo():
# a = PaymentDeviceClient("../server/exampleServer.py")
# a.poll()
# while a.canAccept() == None:
# print "waiting for canAccept"
# a.poll()
# time.sleep(1)
# print a.canAccept()
# a.accept(2341)
# for i in range(4):
# a.poll()
# print a.getCurrentAmount()
# time.sleep(1)
# a.stopAccepting()
# while not a.hasStopped():
# a.poll()
# time.sleep(1)
# print a.getFinalAmountAndReset()
#
# print "can dispense: ..."
# canDispense = None
# while canDispense == None:
# canDispense = a.possibleDispense()
# a.poll()
# time.sleep(1)
# print "can dispense:", canDispense
#
# a.dispense(2341)
#
# while not a.hasStopped():
# a.poll()
# print "waiting for dispense to stop, currently dispensed", a.getCurrentAmount()
# time.sleep(1)
# print "final dispensed:", a.getFinalAmountAndReset()