summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2017-01-14 02:21:45 +0100
committerPiotr Dobrowolski <admin@tastycode.pl>2017-01-14 02:48:07 +0100
commit0d60acf4c9257738cf6370f8871cae04eaa2e3d2 (patch)
tree6cadbfb769f16f330b51280a90526d4f5b3aefe1
parent3f85eab05dafe6be5142c9a3f7c02368e67df0a5 (diff)
downloadbitvend-0d60acf4c9257738cf6370f8871cae04eaa2e3d2.tar.gz
bitvend-0d60acf4c9257738cf6370f8871cae04eaa2e3d2.tar.bz2
bitvend-0d60acf4c9257738cf6370f8871cae04eaa2e3d2.tar.xz
bitvend-0d60acf4c9257738cf6370f8871cae04eaa2e3d2.zip
Refactor out clean python mdb implementation from bitvend
-rw-r--r--bitvend.py21
-rw-r--r--bitvend/mdb.py391
-rw-r--r--mdb/__init__.py1
-rw-r--r--mdb/backend.py70
-rw-r--r--mdb/constants.py28
-rw-r--r--mdb/device.py281
-rw-r--r--mdb/utils.py10
7 files changed, 404 insertions, 398 deletions
diff --git a/bitvend.py b/bitvend.py
index 339114e..b2985c3 100644
--- a/bitvend.py
+++ b/bitvend.py
@@ -36,15 +36,18 @@ def index():
def log():
return flask.render_template(
'log.html', transactions=Transaction.query.all())
-@app.route('/begin/<int:amount>')
-def begin_session(amount):
- dev.begin_session(amount)
- return 'ok'
-
-@app.route('/cancel')
-def cancel_session():
- dev.cancel_session()
- return 'ok'
+
+@app.route('/reclaim/<tx_hash>')
+def reclaim(tx_hash):
+ tx = Transaction.query.filter_by(tx_hash=tx_hash).first()
+
+ if tx and tx.product_id is None:
+ dev.begin_session(tx.value, tx_hash)
+ dev.begin_session(tx.value, tx_hash)
+ dev.begin_session(tx.value, tx_hash)
+ return flask.redirect('/log')
+
+ flask.abort(404)
@app.context_processor
def ctx_utils():
diff --git a/bitvend/mdb.py b/bitvend/mdb.py
index 80dc6e1..acf4b69 100644
--- a/bitvend/mdb.py
+++ b/bitvend/mdb.py
@@ -1,371 +1,10 @@
import time
-import struct
-import logging
-
-try:
- import queue
-except ImportError:
- import Queue as queue
-
-from functools import reduce
from bitvend.models import db, Transaction
from bitvend.stats import cashless_purchase_counter, coin_counter, purchase_counter
-RX_PIN = 4
-TX_PIN = 17
-
-# Page 26 - peripheral addresses
-COIN_TYPE = 0x0c
-COIN_POLL = 0x0b
-
-COIN_EXP = 0x0f
-COIN_EXP_PAYOUT = 0x02
-
-CASHLESS_RESET = 0x10
-CASHLESS_SETUP = 0x11
-CASHLESS_POLL = 0x12
-CASHLESS_VEND = 0x13
-CASHLESS_READER = 0x14
-CASHLESS_REVALUE = 0x15
-
-CASHLESS_EXP = 0x17
-CASHLESS_EXP_ID = 0x00
-CASHLESS_EXP_READ = 0x01
-CASHLESS_EXP_WRITE = 0x02
-CASHLESS_EXP_WRITE_TIME = 0x03
-CASHLESS_EXP_FEATURE_ENABLE = 0x04
-
-BILL_RESET = 0x30
-BILL_SETUP = 0x31
-BILL_POLL = 0x33
-BILL_TYPE = 0x34
-BILL_STACKER = 0x36
-BILL_EXP = 0x37
-BILL_EXP_ID = 0x00
-
-try:
- import pigpio
-except:
- pigpio = None
-
-
-def compute_chk(data):
- return reduce(lambda a, b: (a+b)%256, data, 0)
-
-def compute_checksum(cmd, data):
- return compute_chk(bytearray([cmd]) + data)
-
-def bcd_decode(b):
- return 10 * ((b & 0xf0) >> 4) + (b & 0x0f)
-
-
-class MDBRequest(object):
- timestamp = None
- data = None
- command = None
- processed = False
-
- def __init__(self, command):
- self.timestamp = time.time()
- self.command = command
- self.data = bytearray()
-
- def validate_checksum(self):
- if not self.data:
- return False
-
- try:
- if self.ack:
- if len(self.data) == 1 and self.processed:
- return True # Only ACK
-
- return self.data[-2] == compute_checksum(self.command, self.data[:-2])
- else:
- return self.data[-1] == compute_checksum(self.command, self.data[:-1])
- except KeyboardInterrupt:
- raise
- except:
- logging.exception('Checksum validation failed for: %r %r', self.command, self.data)
- return False
- @property
- def ack(self):
- return self.data[-1] == 0x00
-
- def __repr__(self):
- return '<MDBRequest 0x%02x [%s] chk:%r>' % (
- self.command,
- ' '.join(['0x%02x' % b for b in self.data]),
- self.validate_checksum()
- )
-
-class BackendDevice(object):
- def __init__(self):
- pass
-
- def open(self):
- pass
-
- def close(self):
- pass
-
- def read(self):
- time.sleep(0.005)
- return b''
-
- def write(self, data):
- pass
-
-
-class RaspiDevice(BackendDevice):
- def __init__(self, rx_pin=RX_PIN, tx_pin=TX_PIN):
- self.rx_pin = rx_pin
- self.tx_pin = tx_pin
-
- def open(self):
- self.pi = pigpio.pi()
- self.pi.wave_clear()
- self.pi.set_mode(self.tx_pin, pigpio.INPUT)
-
- try:
- self.pi.bb_serial_read_close(self.rx_pin)
- except:
- pass
-
- status = self.pi.bb_serial_read_open(self.rx_pin, 9600, 9)
- if status:
- raise Exception('Port open failed: %d', status)
-
- def read(self):
- _, data = self.pi.bb_serial_read(self.rx_pin)
- return data
-
- def write(self, data):
- self.pi.wave_clear()
- self.pi.wave_add_serial(self.tx_pin, 9600, data, bb_bits=9, bb_stop=6)
- wid = self.pi.wave_create()
-
- self.pi.set_mode(self.tx_pin, pigpio.OUTPUT)
- self.pi.wave_send_once(wid) # transmit serial data
-
- while self.pi.wave_tx_busy(): # wait until all data sent
- pass
-
- self.pi.wave_delete(wid)
- self.pi.set_mode(self.tx_pin, pigpio.INPUT)
-
-
-class MDBDevice(object):
- current_request = None
- send_buffer = None
-
- def __init__(self, app=None):
- self.logger = logging.getLogger(type(self).__name__)
- self.poll_queue = queue.Queue()
- if pigpio:
- self.backend = RaspiDevice()
- else:
- self.logger.warning('Running with dummy backend device')
- self.backend = BackendDevice()
-
- def initialize(self):
- self.logger.info('Initializing...')
- self.backend.open()
- # here should IO / connection initizliation go
-
- def run(self):
- self.initialize()
-
- while True:
- data = self.backend.read()
- for b in range(0, len(data), 2):
- if data[b+1]:
- if self.current_request: # and not self.current_request.processed:
- self.logger.debug(self.current_request)
- if self.current_request.processed and self.current_request.ack:
- self.logger.info('Got response: %d',self.current_request.data[-1])
-
- self.current_request = MDBRequest(data[b])
- self.send_buffer = None
- elif self.current_request:
- if self.current_request.processed and data[b] == 0xaa and self.send_buffer:
- self.logger.warning('Received RETRANSMIT %r %r', self.current_request, self.send_buffer)
- #self.send(self.send_buffer, checksum=False)
- else:
- self.current_request.data.append(data[b])
- else:
- self.logger.warning('Received unexpected data: 0x%02x', data[b])
-
- if self.current_request and not self.current_request.processed:
- try:
- resp = self.process_request(self.current_request)
-
- if resp is not None and not self.current_request.processed:
- self.current_request.processed = True
- self.send(resp)
- except KeyboardInterrupt:
- raise
- except:
- self.logger.exception('Request process failed!')
-
- def send(self, data, checksum=True):
- data = list(data)
- if checksum:
- data.append(0x100 | compute_chk(data))
-
- msg = struct.pack('<%dh' % len(data), *data)
-
- self.send_buffer = data
-
- self.logger.debug('>> [%d bytes] %s', len(data), ' '.join(['0x%02x' % b for b in data]))
-
- self.backend.write(msg)
-
- def process_request(self, req):
- # Unimplemented...
- return
-
-#
-# This is mostly working cashless device implementation
-#
-
-class CashlessMDBDevice(MDBDevice):
- base_address = CASHLESS_RESET
- state = 'IDLE'
-
- config_data = [
- 0x01, # Feature level
- 0x19, 0x85, # PLN x---DD
- 1, # scaling factor
- 0x00, # decimal places factor
- 10, # 10s response time
- 0x00, # misc options...
- ]
-
- manufacturer = 'GMD'
- serial_number = '123456789012'
- model_number = '123456789012'
- software_version = (0x21, 0x37)
-
- def process_request(self, req):
- if (req.command & self.base_address) != self.base_address:
- # Target mismatch
- return
-
- if not req.validate_checksum():
- # Invalid checksum
- return
-
- if req.command == CASHLESS_RESET:
- self.state = 'RESET'
- self.logger.info('RESET: Device reset')
- self.poll_queue.put([0x00])
-
- return []
-
- elif req.command == CASHLESS_POLL:
- try:
- msg = self.poll_queue.get_nowait()
- self.logger.info('Sending POLL response: %r', msg)
- return msg
- except queue.Empty:
- return []
-
- #if self.state == 'RESET':
- # self.state = 'IDLE'
- # self.logger.info('POLL: Sending JUST RESET')
- # return [0b00] # validator was reset
- #elif self.state == 'IDLE':
- # self.state = 'SESSION'
- # self.logger.info('POLL: starting session')
- # return [0x03, 0xff, 0xff] # goes up to 65535 x---DD
- #else:
- # return []
- # #return([0x02, 10, ord('J'), ord('P'), ord('P')] + [ord('X')] * (32-3))
-
- elif req.command == CASHLESS_VEND:
- if req.data[0] == 0x00: # vend request
- self.logger.info('VEND: request %r', req)
- value, product_bcd = struct.unpack('>xhhx', req.data)
- product = bcd_decode(product_bcd)
- #self.logger.info('VEND: requested %d for %d', product, value)
- if self.vend_request(product, value):
- # accept. two latter bytes are value subtracted from balance
- # displayed after purchase
- return [0x05, 0x00, 0xff]
- else:
- # welp?
- return [0x06]
-
- elif req.data[0] == 0x01: # vend cancel
- self.logger.info('VEND: cancel')
- return [0x06] # deny == ok
-
- elif req.data[0] == 0x02: # vend succ
- self.logger.info('VEND: success %r', req)
- return []
-
- elif req.data[0] == 0x03:
- self.logger.info('VEND: failure')
- return []
-
- elif req.data[0] == 0x04:
- self.logger.info('VEND: session complete %r', req)
- self.state = 'IDLE'
- return [0x07]
-
- elif req.data[0] == 0x05:
- self.logger.info('VEND: cash sale')
- return []
-
- elif req.data[0] == 0x06:
- self.logger.info('VEND: negative vend request')
- return [0x06] # deny
- else:
- self.logger.warning('VEND: unknown command %r', req)
-
- elif req.command == CASHLESS_EXP and req.data[0] == CASHLESS_EXP_ID:
- self.logger.info('EXP_ID request')
- return (
- bytearray([0x09]) + # peripheral ID
- bytearray(self.manufacturer.rjust(3, ' ').encode('ascii')) +
- bytearray(self.serial_number.rjust(12, '0').encode('ascii')) +
- bytearray(self.model_number.rjust(12, '0').encode('ascii')) +
- bytearray(self.software_version)
- )
-
- elif req.command == CASHLESS_SETUP and req.data[0] == 0x00 and len(req.data) == 6:
- vmc_level, disp_cols, disp_rows, disp_info = req.data[1:-1]
-
- self.logger.info('SETUP config')
- self.logger.info(' -> VMC level: %d', vmc_level)
- self.logger.info(' -> Disp cols: %d', disp_cols)
- self.logger.info(' -> Disp rows: %d', disp_rows)
- self.logger.info(' -> Disp info: %d', disp_info)
-
- self.state = 'IDLE'
-
- return [0x01] + self.config_data
-
- elif req.command == CASHLESS_SETUP and req.data[0] == 0x01:
- self.logger.info('SETUP max/min price: %r', req)
- return []
-
- elif req.command == CASHLESS_READER:
- self.logger.info('READER update: %r', req.data[0])
- return []
-
- def begin_session(self, amount):
- if amount > 65535:
- amount = 65535
-
- self.poll_queue.put([0x03, (amount >> 8) & 0xff, amount & 0xff])
-
- def cancel_session(self):
- self.poll_queue.put([0x04])
-
- def vend_request(self, product, value):
- return True
+from mdb.device import CashlessMDBDevice
+from mdb.constants import *
class BitvendCashlessMDBDevice(CashlessMDBDevice):
@@ -417,29 +56,3 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
coin_counter.inc()
return super(BitvendCashlessMDBDevice, self).process_request(req)
-
-
-#
-# This is mostly unfinished Bill validator implementation
-#
-
-class BillMDBDevice(MDBDevice):
- scaling_factor = 50
-
- bills = [
- 50, 100, 200, 500, 1000, 2000, 5000, 10000,
- 0, 0, 0, 0, 0, 0, 0, 0,
- ]
-
- feed_bills = []
-
- def feed_amount(self, amount):
- if amount % self.scaling_factor:
- raise Exception('Invalid amount')
-
- while amount > 0:
- bills_list = filter(lambda v: v <= amount, self.bills)
- bills_list.sort()
-
- self.feed_bills.append(self.bills.index(bills_list[-1]))
- amount -= bills_list[-1]
diff --git a/mdb/__init__.py b/mdb/__init__.py
new file mode 100644
index 0000000..70c5ff2
--- /dev/null
+++ b/mdb/__init__.py
@@ -0,0 +1 @@
+from mdb.device import MDBDevice, CashlessMDBDevice
diff --git a/mdb/backend.py b/mdb/backend.py
new file mode 100644
index 0000000..256c836
--- /dev/null
+++ b/mdb/backend.py
@@ -0,0 +1,70 @@
+import time
+
+try:
+ import pigpio
+except:
+ pigpio = None
+
+RX_PIN = 4
+TX_PIN = 17
+
+
+class Backend(object):
+ def __init__(self):
+ pass
+
+ def open(self):
+ pass
+
+ def close(self):
+ pass
+
+ def read(self):
+ return b''
+
+ def write(self, data):
+ pass
+
+
+class DummyBackend(Backend):
+ def read(self):
+ time.sleep(0.005)
+ return b''
+
+
+class RaspiBackend(Backend):
+ def __init__(self, rx_pin=RX_PIN, tx_pin=TX_PIN):
+ self.rx_pin = rx_pin
+ self.tx_pin = tx_pin
+
+ def open(self):
+ self.pi = pigpio.pi()
+ self.pi.wave_clear()
+ self.pi.set_mode(self.tx_pin, pigpio.INPUT)
+
+ try:
+ self.pi.bb_serial_read_close(self.rx_pin)
+ except:
+ pass
+
+ status = self.pi.bb_serial_read_open(self.rx_pin, 9600, 9)
+ if status:
+ raise Exception('Port open failed: %d', status)
+
+ def read(self):
+ _, data = self.pi.bb_serial_read(self.rx_pin)
+ return data
+
+ def write(self, data):
+ self.pi.wave_clear()
+ self.pi.wave_add_serial(self.tx_pin, 9600, data, bb_bits=9, bb_stop=6)
+ wid = self.pi.wave_create()
+
+ self.pi.set_mode(self.tx_pin, pigpio.OUTPUT)
+ self.pi.wave_send_once(wid) # transmit serial data
+
+ while self.pi.wave_tx_busy(): # wait until all data sent
+ pass
+
+ self.pi.wave_delete(wid)
+ self.pi.set_mode(self.tx_pin, pigpio.INPUT)
diff --git a/mdb/constants.py b/mdb/constants.py
new file mode 100644
index 0000000..f6231ef
--- /dev/null
+++ b/mdb/constants.py
@@ -0,0 +1,28 @@
+# Page 26 - peripheral addresses
+COIN_TYPE = 0x0c
+COIN_POLL = 0x0b
+
+COIN_EXP = 0x0f
+COIN_EXP_PAYOUT = 0x02
+
+CASHLESS_RESET = 0x10
+CASHLESS_SETUP = 0x11
+CASHLESS_POLL = 0x12
+CASHLESS_VEND = 0x13
+CASHLESS_READER = 0x14
+CASHLESS_REVALUE = 0x15
+
+CASHLESS_EXP = 0x17
+CASHLESS_EXP_ID = 0x00
+CASHLESS_EXP_READ = 0x01
+CASHLESS_EXP_WRITE = 0x02
+CASHLESS_EXP_WRITE_TIME = 0x03
+CASHLESS_EXP_FEATURE_ENABLE = 0x04
+
+BILL_RESET = 0x30
+BILL_SETUP = 0x31
+BILL_POLL = 0x33
+BILL_TYPE = 0x34
+BILL_STACKER = 0x36
+BILL_EXP = 0x37
+BILL_EXP_ID = 0x00
diff --git a/mdb/device.py b/mdb/device.py
new file mode 100644
index 0000000..2dce894
--- /dev/null
+++ b/mdb/device.py
@@ -0,0 +1,281 @@
+import time
+import struct
+import logging
+
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from mdb.utils import compute_checksum, compute_chk, bcd_decode
+from mdb.constants import *
+from mdb.backend import RaspiBackend, DummyBackend, pigpio
+
+class MDBRequest(object):
+ timestamp = None
+ data = None
+ command = None
+ processed = False
+
+ def __init__(self, command):
+ self.timestamp = time.time()
+ self.command = command
+ self.data = bytearray()
+
+ def validate_checksum(self):
+ if not self.data:
+ return False
+
+ try:
+ if self.ack:
+ if len(self.data) == 1 and self.processed:
+ return True # Only ACK
+
+ return self.data[-2] == compute_checksum(self.command, self.data[:-2])
+ else:
+ return self.data[-1] == compute_checksum(self.command, self.data[:-1])
+ except KeyboardInterrupt:
+ raise
+ except:
+ logging.exception('Checksum validation failed for: %r %r', self.command, self.data)
+ return False
+ @property
+ def ack(self):
+ return self.data[-1] == 0x00
+
+ def __repr__(self):
+ return '<MDBRequest 0x%02x [%s] chk:%r>' % (
+ self.command,
+ ' '.join(['0x%02x' % b for b in self.data]),
+ self.validate_checksum()
+ )
+
+
+class MDBDevice(object):
+ current_request = None
+ send_buffer = None
+
+ def __init__(self, app=None):
+ self.logger = logging.getLogger(type(self).__name__)
+ self.poll_queue = queue.Queue()
+ if pigpio:
+ self.backend = RaspiBackend()
+ else:
+ self.logger.warning('Running with dummy backend device')
+ self.backend = DummyBackend()
+
+ def initialize(self):
+ self.logger.info('Initializing...')
+ self.backend.open()
+ # here should IO / connection initizliation go
+
+ def run(self):
+ self.initialize()
+
+ while True:
+ data = self.backend.read()
+ for b in range(0, len(data), 2):
+ if data[b+1]:
+ if self.current_request: # and not self.current_request.processed:
+ self.logger.debug(self.current_request)
+ if self.current_request.processed and self.current_request.ack:
+ self.logger.info('Got response: %d',self.current_request.data[-1])
+
+ self.current_request = MDBRequest(data[b])
+ self.send_buffer = None
+ elif self.current_request:
+ if self.current_request.processed and data[b] == 0xaa and self.send_buffer:
+ self.logger.warning('Received RETRANSMIT %r %r', self.current_request, self.send_buffer)
+ #self.send(self.send_buffer, checksum=False)
+ else:
+ self.current_request.data.append(data[b])
+ else:
+ self.logger.warning('Received unexpected data: 0x%02x', data[b])
+
+ if self.current_request and not self.current_request.processed:
+ try:
+ resp = self.process_request(self.current_request)
+
+ if resp is not None and not self.current_request.processed:
+ self.current_request.processed = True
+ self.send(resp)
+ except KeyboardInterrupt:
+ raise
+ except:
+ self.logger.exception('Request process failed!')
+
+ def send(self, data, checksum=True):
+ data = list(data)
+ if checksum:
+ data.append(0x100 | compute_chk(data))
+
+ msg = struct.pack('<%dh' % len(data), *data)
+
+ self.send_buffer = data
+
+ self.logger.debug('>> [%d bytes] %s', len(data), ' '.join(['0x%02x' % b for b in data]))
+
+ self.backend.write(msg)
+
+ def process_request(self, req):
+ # Unimplemented...
+ return
+
+#
+# This is mostly working cashless device implementation
+#
+
+class CashlessMDBDevice(MDBDevice):
+ base_address = CASHLESS_RESET
+ state = 'IDLE'
+
+ config_data = [
+ 0x01, # Feature level
+ 0x19, 0x85, # PLN x---DD
+ 1, # scaling factor
+ 0x00, # decimal places factor
+ 10, # 10s response time
+ 0x00, # misc options...
+ ]
+
+ manufacturer = 'GMD'
+ serial_number = '123456789012'
+ model_number = '123456789012'
+ software_version = (0x21, 0x37)
+
+ def process_request(self, req):
+ if (req.command & self.base_address) != self.base_address:
+ # Target mismatch
+ return
+
+ if not req.validate_checksum():
+ # Invalid checksum
+ return
+
+ if req.command == CASHLESS_RESET:
+ self.state = 'RESET'
+ self.logger.info('RESET: Device reset')
+ self.poll_queue.put([0x00])
+
+ return []
+
+ elif req.command == CASHLESS_POLL:
+ try:
+ msg = self.poll_queue.get_nowait()
+ self.logger.info('Sending POLL response: %r', msg)
+ return msg
+ except queue.Empty:
+ return []
+
+ elif req.command == CASHLESS_VEND:
+ if req.data[0] == 0x00: # vend request
+ self.logger.info('VEND: request %r', req)
+ value, product_bcd = struct.unpack('>xhhx', req.data)
+ product = bcd_decode(product_bcd)
+ self.logger.info('VEND: requested %d for %d', product, value)
+ if self.vend_request(product, value):
+ # accept. two latter bytes are value subtracted from balance
+ # displayed after purchase FIXME
+ return [0x05, 0x00, 0xff]
+ else:
+ # welp?
+ return [0x06]
+
+ elif req.data[0] == 0x01: # vend cancel
+ self.logger.info('VEND: cancel')
+ return [0x06] # deny == ok
+
+ elif req.data[0] == 0x02: # vend succ
+ self.logger.info('VEND: success %r', req)
+ return []
+
+ elif req.data[0] == 0x03:
+ self.logger.info('VEND: failure')
+ return []
+
+ elif req.data[0] == 0x04:
+ self.logger.info('VEND: session complete %r', req)
+ self.state = 'IDLE'
+ return [0x07]
+
+ elif req.data[0] == 0x05:
+ self.logger.info('VEND: cash sale')
+ return []
+
+ elif req.data[0] == 0x06:
+ self.logger.info('VEND: negative vend request')
+ return [0x06] # deny
+ else:
+ self.logger.warning('VEND: unknown command %r', req)
+
+ elif req.command == CASHLESS_EXP and req.data[0] == CASHLESS_EXP_ID:
+ self.logger.info('EXP_ID request')
+ return (
+ bytearray([0x09]) + # peripheral ID
+ bytearray(self.manufacturer.rjust(3, ' ').encode('ascii')) +
+ bytearray(self.serial_number.rjust(12, '0').encode('ascii')) +
+ bytearray(self.model_number.rjust(12, '0').encode('ascii')) +
+ bytearray(self.software_version)
+ )
+
+ elif req.command == CASHLESS_SETUP and req.data[0] == 0x00 and len(req.data) == 6:
+ vmc_level, disp_cols, disp_rows, disp_info = req.data[1:-1]
+
+ self.logger.info('SETUP config')
+ self.logger.info(' -> VMC level: %d', vmc_level)
+ self.logger.info(' -> Disp cols: %d', disp_cols)
+ self.logger.info(' -> Disp rows: %d', disp_rows)
+ self.logger.info(' -> Disp info: %d', disp_info)
+
+ self.state = 'IDLE'
+
+ return [0x01] + self.config_data
+
+ elif req.command == CASHLESS_SETUP and req.data[0] == 0x01:
+ self.logger.info('SETUP max/min price: %r', req)
+ return []
+
+ elif req.command == CASHLESS_READER:
+ self.logger.info('READER update: %r', req.data[0])
+ return []
+
+ def begin_session(self, amount):
+ # Begins new session with balance provided
+ if amount > 65535:
+ amount = 65535
+
+ self.poll_queue.put([0x03, (amount >> 8) & 0xff, amount & 0xff])
+
+ def cancel_session(self):
+ # Cancels current session
+ self.poll_queue.put([0x04])
+
+ def vend_request(self, product, value):
+ # Called when user selects a product
+ return True
+
+
+#
+# This is mostly unfinished Bill validator implementation
+#
+
+class BillMDBDevice(MDBDevice):
+ scaling_factor = 50
+
+ bills = [
+ 50, 100, 200, 500, 1000, 2000, 5000, 10000,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ ]
+
+ feed_bills = []
+
+ def feed_amount(self, amount):
+ if amount % self.scaling_factor:
+ raise Exception('Invalid amount')
+
+ while amount > 0:
+ bills_list = filter(lambda v: v <= amount, self.bills)
+ bills_list.sort()
+
+ self.feed_bills.append(self.bills.index(bills_list[-1]))
+ amount -= bills_list[-1]
diff --git a/mdb/utils.py b/mdb/utils.py
new file mode 100644
index 0000000..37459fa
--- /dev/null
+++ b/mdb/utils.py
@@ -0,0 +1,10 @@
+from functools import reduce
+
+def compute_chk(data):
+ return reduce(lambda a, b: (a+b)%256, data, 0)
+
+def compute_checksum(cmd, data):
+ return compute_chk(bytearray([cmd]) + data)
+
+def bcd_decode(b):
+ return 10 * ((b & 0xf0) >> 4) + (b & 0x0f)