summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bitvend.py358
1 files changed, 358 insertions, 0 deletions
diff --git a/bitvend.py b/bitvend.py
new file mode 100644
index 0000000..45f6c86
--- /dev/null
+++ b/bitvend.py
@@ -0,0 +1,358 @@
+import time
+import struct
+import logging
+import threading
+import flask
+
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from functools import reduce
+
+logging.basicConfig(level=logging.INFO)
+
+#from prometheus_client import start_http_server, Counter
+#start_http_server(8000)
+
+#coin_counter = Counter('coins_inserted', 'Number of coins inserted into machine')
+#purchase_counter = Counter('purchases', 'Number of purchases')
+
+RX_PIN = 4
+TX_PIN = 17
+
+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
+
+# Page 26 - peripheral addresses
+try:
+ import pigpio
+
+ pi = pigpio.pi()
+ pi.wave_clear()
+ pi.set_mode(TX_PIN, pigpio.INPUT)
+
+ try:
+ pi.bb_serial_read_close(RX_PIN)
+ except:
+ pass
+
+ status = pi.bb_serial_read_open(RX_PIN, 9600, 9)
+
+ if status != 0:
+ logging.error('Open failed: %d', status)
+ exit(1)
+except:
+ pass
+
+
+def compute_chk(data):
+ return reduce(lambda a, b: (a+b)%256, data, 0)
+
+def compute_checksum(cmd, data):
+ return reduce(lambda a, b: (a+b)%256, bytearray([cmd]) + data, 0)
+
+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:
+ 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):
+ self.logger = logging.getLogger(type(self).__name__)
+ self.poll_queue = queue.Queue()
+
+ def initialize(self):
+ self.logger.info('Initializing...')
+ # here should IO / connection initizliation go
+
+ def run(self):
+ self.initialize()
+
+ while True:
+ cnt, data = pi.bb_serial_read(RX_PIN)
+ for b in range(0, cnt, 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:
+ self.current_request.processed = True
+ self.send(resp)
+ 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 = msg
+
+ self.logger.debug('>> %s', ' '.join(['0x%02x' % b for b in msg]))
+
+ pi.wave_clear()
+ pi.wave_add_serial(TX_PIN, 9600, msg, bb_bits=9, bb_stop=6)
+ wid = pi.wave_create()
+
+ pi.set_mode(TX_PIN, pigpio.OUTPUT)
+ pi.wave_send_once(wid) # transmit serial data
+
+ while pi.wave_tx_busy(): # wait until all data sent
+ pass
+
+ pi.wave_delete(wid)
+ pi.set_mode(TX_PIN, pigpio.INPUT)
+
+ 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)
+ return [0x05] # , 0xff, 0xff] # accept
+
+ 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: 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])
+
+#
+# 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]
+
+dev = CashlessMDBDevice()
+
+app = flask.Flask(__name__)
+
+@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'
+
+if __name__ == "__main__":
+ threading.Thread(target=app.run, kwargs={'host': '0.0.0.0'}, daemon=True).start()
+ dev.run()