Refactor out clean python mdb implementation from bitvend
parent
3f85eab05d
commit
0d60acf4c9
19
bitvend.py
19
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():
|
||||
|
|
391
bitvend/mdb.py
391
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]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from mdb.device import MDBDevice, CashlessMDBDevice
|
|
@ -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)
|
|
@ -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
|
|
@ -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]
|
|
@ -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)
|
Loading…
Reference in New Issue