318 lines
10 KiB
Python
318 lines
10 KiB
Python
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, SerialBackend, 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
|
|
|
|
poll_msg = []
|
|
|
|
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 = SerialBackend()
|
|
|
|
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:
|
|
if self.current_request.command not in [0xf2]:
|
|
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.poll_msg = []
|
|
|
|
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'
|
|
last_poll = 0
|
|
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)
|
|
|
|
lockup_counter = 0
|
|
|
|
def process_request(self, req):
|
|
# FIXME this shouldn't be required...
|
|
#if req.command == 0x30 and req.validate_checksum():
|
|
# self.lockup_counter += 1
|
|
|
|
# if self.lockup_counter % 50 == 0:
|
|
# self.logger.info('YOLO')
|
|
# return []
|
|
# return
|
|
#if req.command == 0x31 and req.validate_checksum():
|
|
# return []
|
|
#if req.command == 0x37 and req.validate_checksum():
|
|
# return []
|
|
#if req.command == 0x36 and req.validate_checksum():
|
|
# return []
|
|
#if req.command == 0x34 and req.validate_checksum():
|
|
# return []
|
|
|
|
if (req.command & self.base_address) != self.base_address:
|
|
# Target mismatch
|
|
return
|
|
|
|
if not req.validate_checksum():
|
|
# Invalid checksum
|
|
return
|
|
|
|
self.lockup_counter = 0
|
|
|
|
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:
|
|
self.last_poll = time.time()
|
|
|
|
if not self.poll_msg:
|
|
try:
|
|
self.poll_msg = self.poll_queue.get_nowait()
|
|
except queue.Empty:
|
|
pass
|
|
|
|
if self.poll_msg:
|
|
self.logger.info('Sending POLL response: %r', self.poll_msg)
|
|
|
|
return self.poll_msg
|
|
|
|
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 = 'OK'
|
|
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: %r', req)
|
|
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 = 'OK'
|
|
|
|
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):
|
|
self.logger.info('Beginning session for %d', 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
|
|
|
|
@property
|
|
def online(self):
|
|
return time.time() - self.last_poll < 5
|
|
|
|
|
|
#
|
|
# 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]
|