diff options
-rw-r--r-- | bitvend.py | 358 |
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() |