summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2017-01-13 23:42:10 +0100
committerPiotr Dobrowolski <admin@tastycode.pl>2017-01-13 23:42:10 +0100
commit4215faffd1225f8891cb378f0db295f07c3972f6 (patch)
tree68e8fb0518093b271f0881bf69c4ca6b8206bf98
parent8da7e9538fe139662efbab46f2eda308cf441166 (diff)
downloadbitvend-4215faffd1225f8891cb378f0db295f07c3972f6.tar.gz
bitvend-4215faffd1225f8891cb378f0db295f07c3972f6.tar.bz2
bitvend-4215faffd1225f8891cb378f0db295f07c3972f6.tar.xz
bitvend-4215faffd1225f8891cb378f0db295f07c3972f6.zip
Initial fully working implementation
-rw-r--r--.gitignore1
-rw-r--r--bitvend.py411
-rw-r--r--bitvend/__init__.py0
-rw-r--r--bitvend/mdb.py375
-rw-r--r--bitvend/processor.py94
-rw-r--r--bitvend/utils.py25
-rw-r--r--requirements.txt11
-rw-r--r--static/img/club-mate.pngbin0 -> 125768 bytes
-rw-r--r--static/img/promicro.pngbin0 -> 378157 bytes
-rw-r--r--templates/index.html50
10 files changed, 586 insertions, 381 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/bitvend.py b/bitvend.py
index 5470679..737e8b0 100644
--- a/bitvend.py
+++ b/bitvend.py
@@ -1,17 +1,12 @@
-import time
-import struct
import logging
import threading
import flask
-import cachetools
-import requests
+from flask_qrcode import QRcode
-try:
- import queue
-except ImportError:
- import Queue as queue
-
-from functools import reduce
+from bitvend.mdb import CashlessMDBDevice
+from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
+ sat_to_btc
+from bitvend.processor import PaymentProcessor
logging.basicConfig(level=logging.INFO)
@@ -21,376 +16,37 @@ logging.basicConfig(level=logging.INFO)
#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 compute_chk(bytearray([cmd]) + data)
-
-def bcd_decode(b):
- return 10 * ((b & 0xf0) >> 4) + (b & 0x0f)
-
-@cachetools.cached(cachetools.TTLCache(32, 600))
-def get_exchange_rate(currency='PLN'):
- return requests.get('https://blockchain.info/pl/ticker').json()[currency]['last']
-
-def to_local_currency(sat):
- # Returns satoshi in local lowest denomination currency (grosze)
- rate = get_exchange_rate()
- return int(sat / 1000000.0 * rate)
-
-def from_local_currency(val):
- rate = get_exchange_rate()
- return int(val / rate * 1000000)
-
-def sat_to_btc(amount):
- return amount / 100000000.0
-
-def format_btc(amount):
- return (u'฿%.8f' % (sat_to_btc(amount),)).rstrip('0').rstrip('.')
-
-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):
- 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 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]))
-
- 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)
- value, product_bcd = struct.unpack('>xhhx', req.data)
- product = bcd_decode(product_bcd)
- self.logger.info('VEND: requested %d for %d', product, value)
- # accept. two latter bytes are value subtracted from balance
- # displayed after purchase
- return [0x05, 0x00, 0xff]
-
- 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])
-
-#
-# 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()
+proc = PaymentProcessor(dev)
app = flask.Flask(__name__)
+QRcode(app)
+app.config['INPUT_ADDRESS'] = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'
app.config['TEMPLATES_AUTO_RELOAD'] = True
-
+app.config['ITEMS'] = [
+ {
+ 'name': 'Club Mate',
+ 'image': '/static/img/club-mate.png',
+ 'value': 500,
+ },
+ {
+ 'name': 'Arduino Pro Micro',
+ 'image': '/static/img/promicro.png',
+ 'value': 1600,
+ },
+]
+'''
+ {
+ 'name': 'Arduino Pro Mini',
+ 'image': '',
+ 'value': 750,
+ },
+'''
@app.route('/')
def index():
return flask.render_template(
- 'index.html', items=[
- {
- 'name': 'Club Mate',
- 'image': 'http://scrummy.pl/2766-thickbox_default/club-mate.jpg',
- 'value': 500,
- },
- {
- 'name': 'Arduino Pro Micro',
- 'image': 'https://hackerspace.pl/~informatic/dropbox/1d59bf26f1352da558f249d815db6376.png',
- 'value': 0,
- }
- ])
+ 'index.html', items=app.config['ITEMS'])
@app.route('/begin/<int:amount>')
def begin_session(amount):
@@ -399,7 +55,7 @@ def begin_session(amount):
@app.route('/cancel')
def cancel_session():
- dev.cancel_session();
+ dev.cancel_session()
return 'ok'
@app.context_processor
@@ -408,9 +64,12 @@ def ctx_utils():
'from_local_currency': from_local_currency,
'to_local_currency': to_local_currency,
'format_btc': format_btc,
+ 'sat_to_btc': sat_to_btc,
}
if __name__ == "__main__":
- app.run()
- #threading.Thread(target=app.run, kwargs={'host': '0.0.0.0'}, daemon=True).start()
- #dev.run()
+ threading.Thread(target=app.run, kwargs={
+ 'host': '0.0.0.0'
+ }, daemon=True).start()
+ proc.start()
+ dev.run()
diff --git a/bitvend/__init__.py b/bitvend/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bitvend/__init__.py
diff --git a/bitvend/mdb.py b/bitvend/mdb.py
new file mode 100644
index 0000000..4a8c51c
--- /dev/null
+++ b/bitvend/mdb.py
@@ -0,0 +1,375 @@
+import time
+import struct
+import logging
+
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from functools import reduce
+
+
+RX_PIN = 4
+TX_PIN = 17
+
+# Page 26 - peripheral addresses
+
+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:
+ pass
+
+
+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):
+ self.logger = logging.getLogger(type(self).__name__)
+ self.poll_queue = queue.Queue()
+ self.backend = RaspiDevice()
+
+ 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:
+ 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)
+ # accept. two latter bytes are value subtracted from balance
+ # displayed after purchase
+ return [0x05, 0x00, 0xff]
+
+ 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])
+
+#
+# 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]
diff --git a/bitvend/processor.py b/bitvend/processor.py
new file mode 100644
index 0000000..1291940
--- /dev/null
+++ b/bitvend/processor.py
@@ -0,0 +1,94 @@
+import threading
+import json
+import websocket
+import pprint
+import time
+import logging
+
+from bitvend.utils import to_local_currency
+
+
+class PaymentProcessor(threading.Thread):
+ daemon = True
+ input_address = None
+ device = None
+
+ def __init__(self, device, input_address='12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'):
+ super(PaymentProcessor, self).__init__()
+ self.device = device
+ self.input_address = input_address
+ self.logger = logging.getLogger(type(self).__name__)
+
+ def run(self):
+ self.logger.info('Starting...')
+ ws = websocket.WebSocketApp(
+ "wss://ws.blockchain.info/inv",
+ on_message=self.on_message,
+ on_error=self.on_error,
+ on_close=self.on_close)
+
+ ws.on_open = self.on_open
+
+ while True:
+ ws.run_forever()
+
+ def process_transaction(self, tx):
+ tx_size = tx['x']['size']
+ tx_hash = tx['x']['hash']
+ tx_value = sum([
+ o['value'] for o in tx['x']['out'] if o['addr'] == self.input_address
+ ], 0)
+ fee = sum([i['prev_out']['value'] for i in tx['x']['inputs']]) - \
+ sum([o['value'] for o in tx['x']['out']])
+
+ fee_byte = fee / tx_size
+
+ self.logger.info('%r %r %r %r %r', tx_size, tx_hash, tx_value, fee, fee_byte)
+ self.logger.info('In local currency: %r', to_local_currency(tx_value))
+
+ if to_local_currency(tx_value) < 100:
+ self.logger.warning('Whyyyy so low...')
+ return
+
+ if fee_byte < 50:
+ self.logger.warning('Fee too low...')
+ return
+
+ self.logger.info('Transaction ok, going to device...')
+
+ # FIXME we need better handling of ACK on POLL responses...
+ self.device.begin_session(to_local_currency(tx_value))
+ self.device.begin_session(to_local_currency(tx_value))
+ self.device.begin_session(to_local_currency(tx_value))
+
+ def on_message(self, ws, message):
+ #print message
+ data = json.loads(message)
+ if data['op'] == 'utx':
+ self.process_transaction(data)
+
+ def on_error(self, ws, error):
+ self.logger.error(error)
+
+ def on_close(self, ws):
+ self.logger.info('Connection closed')
+
+ def on_open(self, ws):
+ self.logger.info('Connected')
+
+ ws.send(json.dumps({
+ "op": "addr_sub",
+ "addr": self.input_address
+ }))
+
+ threading.Thread(target=self.keepalive, args=(ws,), daemon=True).start()
+
+ def keepalive(self, ws):
+ # Keepalive thread target, just send ping once in a while
+ while True:
+ # FIXME check last ping time
+ self.logger.info('Pinging...')
+ ws.send(json.dumps({
+ "op": "ping"
+ }))
+ time.sleep(20)
diff --git a/bitvend/utils.py b/bitvend/utils.py
new file mode 100644
index 0000000..d360477
--- /dev/null
+++ b/bitvend/utils.py
@@ -0,0 +1,25 @@
+import cachetools
+import requests
+
+@cachetools.cached(cachetools.TTLCache(32, 600))
+def get_exchange_rate(currency='PLN'):
+ # Returns current exchange rate for selected currency
+ return requests.get('https://blockchain.info/pl/ticker').json()[currency]['last']
+
+def to_local_currency(sat):
+ # Returns satoshi in local lowest denomination currency (grosze)
+ rate = get_exchange_rate()
+ return int(sat / 1000000.0 * rate)
+
+def from_local_currency(val):
+ # Returns satoshi value from local currency
+ rate = get_exchange_rate()
+ return int(val / rate * 1000000)
+
+def sat_to_btc(amount):
+ # Converts satoshi to BTC
+ return amount / 100000000.0
+
+def format_btc(amount):
+ # Formats satoshi to human-readable format
+ return (u'฿%.8f' % (sat_to_btc(amount),)).rstrip('0').rstrip('.')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..47941f6
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,11 @@
+cachetools==2.0.0
+click==6.7
+Flask==0.12
+itsdangerous==0.24
+Jinja2==2.9.4
+MarkupSafe==0.23
+pkg-resources==0.0.0
+requests==2.12.4
+six==1.10.0
+websocket-client==0.40.0
+Werkzeug==0.11.15
diff --git a/static/img/club-mate.png b/static/img/club-mate.png
new file mode 100644
index 0000000..3267bbf
--- /dev/null
+++ b/static/img/club-mate.png
Binary files differ
diff --git a/static/img/promicro.png b/static/img/promicro.png
new file mode 100644
index 0000000..a3cab8f
--- /dev/null
+++ b/static/img/promicro.png
Binary files differ
diff --git a/templates/index.html b/templates/index.html
index 3258d08..b6e88c1 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -16,6 +16,14 @@
body {
margin-top: 80px;
}
+ h3 {
+ margin: 0;
+ padding: 0;
+ padding-bottom: 0.5em;
+ }
+ footer {
+ padding-bottom: 20px;
+ }
</style>
</head>
<body>
@@ -29,20 +37,52 @@
<span class="icon-bar"></span>
</button>
</div>
+ <div class="navbar-collapse collapse" id="navbar-main">
+ <p class="navbar-text navbar-right">
+ 1zł = {{ format_btc(from_local_currency(100)) }}
+ </p>
+ <p class="navbar-text navbar-right">
+ <b>Rate:</b> {{ to_local_currency(100000000) / 100 }}zł
+ </p>
+ </div>
</div>
</div>
<div class="container">
+ <div class="row">
{% for item in items %}
- <div class="col-md-3">
+ <div class="col-sm-6">
<div class="well vend-item">
- <img src="{{ item.image }}" class="img-responsive" />
- <span class="label label-default pull-right">{{ '%1.2fzł'|format(item.value/100) }} / {{ format_btc(from_local_currency(item.value)) }}</span>
- <big>{{ item.name }}</big>
- <img src="https://blockchain.info/qr?data=bitcoin:12ihVyoAGRci1K5hGQaQveEy7ThfwXfNSS&amount={{ from_local_currency(item.value) }}&size=288" class="img-responsive" />
+ <div class="row">
+ <div class="col-md-12">
+ <div class="pull-right">
+ <span class="label label-info">{{ '%.2fzł'|format(item.value/100) }}</span>
+ <span class="label label-primary">{{ format_btc(from_local_currency(item.value*1.1)) }}</span>
+ </div>
+ <h3>{{ item.name }}</h3>
+ </div>
+ <div class="col-xs-6">
+ <img src="{{ item.image }}" class="img-responsive center-block" />
+ </div>
+ <div class="col-xs-6">
+ {% with btc_uri = 'bitcoin:%s?amount=%s' % (config['INPUT_ADDRESS'], sat_to_btc(from_local_currency(item.value*1.1))) %}
+ <a href="{{ btc_uri }}">
+ <img src="{{ qrcode(btc_uri) }}" class="img-responsive center-block"/>
+ <center><code><small>{{ config['INPUT_ADDRESS'] }}</small></code></center>
+ </a>
+ {% endwith %}
+ </div>
+ </div>
</div>
</div>
{% endfor %}
+ </div>
+
+ <hr>
+
+ <footer class="row"><big>made by <a href="https://wiki.hackerspace.pl/people:informatic:start">inf</a></big>
+ <span class="pull-right text-right">1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW &bull; <a href="https://code.hackerspace.pl/informatic/bitvend">sauce</a><br />report any issues to <a href="mailto:informatic@hackerspace.pl">informatic@hackerspace.pl</a></span>
+ </footer>
</div>
</body>
</html>