summaryrefslogtreecommitdiffstats
path: root/bitvend/mdb.py
diff options
context:
space:
mode:
Diffstat (limited to 'bitvend/mdb.py')
-rw-r--r--bitvend/mdb.py391
1 files changed, 2 insertions, 389 deletions
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]