summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2017-01-14 02:04:41 +0100
committerPiotr Dobrowolski <admin@tastycode.pl>2017-01-14 02:05:25 +0100
commit3f85eab05dafe6be5142c9a3f7c02368e67df0a5 (patch)
treee1267a104416dc7ed133d834c08befb482d489a7
parent4215faffd1225f8891cb378f0db295f07c3972f6 (diff)
downloadbitvend-3f85eab05dafe6be5142c9a3f7c02368e67df0a5.tar.gz
bitvend-3f85eab05dafe6be5142c9a3f7c02368e67df0a5.tar.bz2
bitvend-3f85eab05dafe6be5142c9a3f7c02368e67df0a5.tar.xz
bitvend-3f85eab05dafe6be5142c9a3f7c02368e67df0a5.zip
Add storage/models, bring back stats
-rw-r--r--bitvend.py49
-rw-r--r--bitvend/default_settings.py29
-rw-r--r--bitvend/mdb.py86
-rw-r--r--bitvend/models.py14
-rw-r--r--bitvend/processor.py35
-rw-r--r--bitvend/stats.py5
-rw-r--r--static/img/nodemcu.pngbin0 -> 337529 bytes
-rw-r--r--static/img/promini.pngbin0 -> 152284 bytes
-rw-r--r--templates/base.html58
-rw-r--r--templates/index.html113
-rw-r--r--templates/log.html16
11 files changed, 279 insertions, 126 deletions
diff --git a/bitvend.py b/bitvend.py
index 737e8b0..339114e 100644
--- a/bitvend.py
+++ b/bitvend.py
@@ -3,51 +3,39 @@ import threading
import flask
from flask_qrcode import QRcode
-from bitvend.mdb import CashlessMDBDevice
+from bitvend.mdb import BitvendCashlessMDBDevice
from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
sat_to_btc
from bitvend.processor import PaymentProcessor
+from bitvend.models import db, Transaction
logging.basicConfig(level=logging.INFO)
-#from prometheus_client import start_http_server, Counter
-#start_http_server(8000)
+from prometheus_client import start_http_server
+start_http_server(8000)
-#coin_counter = Counter('coins_inserted', 'Number of coins inserted into machine')
-#purchase_counter = Counter('purchases', 'Number of purchases')
+app = flask.Flask(__name__)
+app.config.from_object('bitvend.default_settings')
-dev = CashlessMDBDevice()
-proc = PaymentProcessor(dev)
+db.init_app(app)
-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,
- },
-'''
+dev = BitvendCashlessMDBDevice()
+dev.init_app(app)
+
+proc = PaymentProcessor(dev)
+proc.init_app(app)
+
@app.route('/')
def index():
return flask.render_template(
'index.html', items=app.config['ITEMS'])
+@app.route('/log')
+def log():
+ return flask.render_template(
+ 'log.html', transactions=Transaction.query.all())
@app.route('/begin/<int:amount>')
def begin_session(amount):
dev.begin_session(amount)
@@ -68,6 +56,9 @@ def ctx_utils():
}
if __name__ == "__main__":
+ with app.app_context():
+ db.create_all()
+
threading.Thread(target=app.run, kwargs={
'host': '0.0.0.0'
}, daemon=True).start()
diff --git a/bitvend/default_settings.py b/bitvend/default_settings.py
new file mode 100644
index 0000000..5c79f21
--- /dev/null
+++ b/bitvend/default_settings.py
@@ -0,0 +1,29 @@
+import platform
+
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+SQLALCHEMY_DATABASE_URI = 'sqlite:///storage-%s.db' % (platform.node(),)
+
+INPUT_ADDRESS = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'
+TEMPLATES_AUTO_RELOAD = True
+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': '/static/img/promini.png',
+ 'value': 750,
+ },
+ {
+ 'name': 'NodeMCU (ESP8266)',
+ 'image': '/static/img/nodemcu.png',
+ 'value': 1700,
+ },
+]
diff --git a/bitvend/mdb.py b/bitvend/mdb.py
index 4a8c51c..80dc6e1 100644
--- a/bitvend/mdb.py
+++ b/bitvend/mdb.py
@@ -9,11 +9,18 @@ except ImportError:
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
@@ -40,7 +47,7 @@ BILL_EXP_ID = 0x00
try:
import pigpio
except:
- pass
+ pigpio = None
def compute_chk(data):
@@ -152,10 +159,14 @@ class MDBDevice(object):
current_request = None
send_buffer = None
- def __init__(self):
+ def __init__(self, app=None):
self.logger = logging.getLogger(type(self).__name__)
self.poll_queue = queue.Queue()
- self.backend = RaspiDevice()
+ if pigpio:
+ self.backend = RaspiDevice()
+ else:
+ self.logger.warning('Running with dummy backend device')
+ self.backend = BackendDevice()
def initialize(self):
self.logger.info('Initializing...')
@@ -189,7 +200,7 @@ class MDBDevice(object):
try:
resp = self.process_request(self.current_request)
- if resp is not None:
+ if resp is not None and not self.current_request.processed:
self.current_request.processed = True
self.send(resp)
except KeyboardInterrupt:
@@ -277,10 +288,14 @@ class CashlessMDBDevice(MDBDevice):
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]
+ #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')
@@ -349,6 +364,61 @@ class CashlessMDBDevice(MDBDevice):
def cancel_session(self):
self.poll_queue.put([0x04])
+ def vend_request(self, product, value):
+ return True
+
+
+class BitvendCashlessMDBDevice(CashlessMDBDevice):
+ current_tx_hash = None
+ app = None
+
+ def init_app(self, app):
+ self.app = app
+
+ def begin_session(self, amount, tx_hash=None):
+ self.current_tx_hash = tx_hash
+
+ super(BitvendCashlessMDBDevice, self).begin_session(amount)
+
+ def vend_request(self, product, value):
+ #self.logger.info('got vend request: %r', self.current_tx_hash)
+ self.send([0x05, 0x00, 0xff])
+ self.current_request.processed = True
+
+ if self.current_tx_hash:
+ with self.app.app_context():
+ tx = Transaction.query.filter_by(tx_hash=self.current_tx_hash).first()
+ tx.product_id = product
+ tx.product_value = value
+ db.session.commit()
+
+ cashless_purchase_counter.inc()
+
+ return True
+
+ last_purchase = 0
+
+ def process_request(self, req):
+ if req.command == COIN_EXP and req.validate_checksum() and req.data[0] == COIN_EXP_PAYOUT and not req.processed:
+ self.logger.info('Purchase with change detected')
+ req.processed = True
+ if time.time() - self.last_purchase > 5:
+ purchase_counter.inc()
+ self.last_purchase = time.time()
+ elif req.command == COIN_TYPE and req.validate_checksum() and not req.processed:
+ self.logger.info('Purchase without detected')
+ req.processed = True
+ if time.time() - self.last_purchase > 5:
+ purchase_counter.inc()
+ self.last_purchase = time.time()
+ elif req.command == COIN_POLL and req.validate_checksum() and not req.processed and req.ack:
+ self.logger.info('Coin detected')
+ req.processed = True
+ coin_counter.inc()
+
+ return super(BitvendCashlessMDBDevice, self).process_request(req)
+
+
#
# This is mostly unfinished Bill validator implementation
#
diff --git a/bitvend/models.py b/bitvend/models.py
new file mode 100644
index 0000000..88e21b9
--- /dev/null
+++ b/bitvend/models.py
@@ -0,0 +1,14 @@
+from flask_sqlalchemy import SQLAlchemy
+from datetime import datetime
+
+db = SQLAlchemy()
+
+class Transaction(db.Model):
+ __tablename__ = 'transactions'
+ id = db.Column(db.Integer, primary_key=True)
+ tx_hash = db.Column(db.String)
+ created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ value = db.Column(db.Integer)
+ product_id = db.Column(db.Integer)
+ product_value = db.Column(db.Integer)
diff --git a/bitvend/processor.py b/bitvend/processor.py
index 1291940..8b7c9f1 100644
--- a/bitvend/processor.py
+++ b/bitvend/processor.py
@@ -1,24 +1,35 @@
import threading
import json
import websocket
-import pprint
import time
import logging
from bitvend.utils import to_local_currency
+from bitvend.models import db, Transaction
class PaymentProcessor(threading.Thread):
daemon = True
input_address = None
device = None
+ last_pong = None
+ app = None
- def __init__(self, device, input_address='12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'):
+ def __init__(self, device, input_address=None, app=None):
super(PaymentProcessor, self).__init__()
self.device = device
self.input_address = input_address
self.logger = logging.getLogger(type(self).__name__)
+ if app:
+ self.init_app(app)
+
+ def init_app(self, app):
+ self.app = app
+
+ if not self.input_address:
+ self.input_address = self.app.config['INPUT_ADDRESS']
+
def run(self):
self.logger.info('Starting...')
ws = websocket.WebSocketApp(
@@ -46,6 +57,12 @@ class PaymentProcessor(threading.Thread):
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))
+ with self.app.app_context():
+ tx = Transaction(tx_hash=tx_hash)
+ tx.value = to_local_currency(tx_value)
+ db.session.add(tx)
+ db.session.commit()
+
if to_local_currency(tx_value) < 100:
self.logger.warning('Whyyyy so low...')
return
@@ -57,15 +74,18 @@ class PaymentProcessor(threading.Thread):
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))
+ self.device.begin_session(to_local_currency(tx_value), tx_hash)
+ self.device.begin_session(to_local_currency(tx_value), tx_hash)
+ self.device.begin_session(to_local_currency(tx_value), tx_hash)
def on_message(self, ws, message):
#print message
data = json.loads(message)
+
if data['op'] == 'utx':
self.process_transaction(data)
+ elif data['op'] == 'pong':
+ self.last_pong = time.time()
def on_error(self, ws, error):
self.logger.error(error)
@@ -85,10 +105,13 @@ class PaymentProcessor(threading.Thread):
def keepalive(self, ws):
# Keepalive thread target, just send ping once in a while
+ self.last_pong = time.time()
while True:
# FIXME check last ping time
- self.logger.info('Pinging...')
ws.send(json.dumps({
"op": "ping"
}))
time.sleep(20)
+
+ if time.time() - self.last_pong > 60:
+ ws.close()
diff --git a/bitvend/stats.py b/bitvend/stats.py
new file mode 100644
index 0000000..a00cce1
--- /dev/null
+++ b/bitvend/stats.py
@@ -0,0 +1,5 @@
+from prometheus_client import start_http_server, Counter
+
+coin_counter = Counter('coins_inserted', 'Number of coins inserted into machine')
+purchase_counter = Counter('purchases', 'Number of purchases')
+cashless_purchase_counter = Counter('cashless_purchases', 'Number of cashless (BTC) purchases')
diff --git a/static/img/nodemcu.png b/static/img/nodemcu.png
new file mode 100644
index 0000000..a8e4219
--- /dev/null
+++ b/static/img/nodemcu.png
Binary files differ
diff --git a/static/img/promini.png b/static/img/promini.png
new file mode 100644
index 0000000..d6faaa0
--- /dev/null
+++ b/static/img/promini.png
Binary files differ
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..77f572b
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>bitvend - The Bitcoin Vending Machine</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <link rel="stylesheet" href="/static/css/bootstrap.css" media="screen">
+ <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="../bower_components/html5shiv/dist/html5shiv.js"></script>
+ <script src="../bower_components/respond/dest/respond.min.js"></script>
+ <![endif]-->
+
+ <style>
+body {
+ margin-top: 80px;
+}
+ h3 {
+ margin: 0;
+ padding: 0;
+ padding-bottom: 0.5em;
+ }
+ footer {
+ padding-bottom: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="navbar navbar-default navbar-fixed-top">
+ <div class="container">
+ <div class="navbar-header">
+ <a href="../" class="navbar-brand">bitvend - bitcoin vending machine</a>
+ <button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-main">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <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">
+ {% block content %}{% endblock %}
+ <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>
diff --git a/templates/index.html b/templates/index.html
index b6e88c1..4474f17 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,88 +1,35 @@
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>bitvend - The Bitcoin Vending Machine</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <link rel="stylesheet" href="/static/css/bootstrap.css" media="screen">
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="../bower_components/html5shiv/dist/html5shiv.js"></script>
- <script src="../bower_components/respond/dest/respond.min.js"></script>
- <![endif]-->
-
- <style>
- body {
- margin-top: 80px;
- }
- h3 {
- margin: 0;
- padding: 0;
- padding-bottom: 0.5em;
- }
- footer {
- padding-bottom: 20px;
- }
- </style>
- </head>
- <body>
- <div class="navbar navbar-default navbar-fixed-top">
- <div class="container">
- <div class="navbar-header">
- <a href="../" class="navbar-brand">bitvend - bitcoin vending machine</a>
- <button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-main">
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <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">
+{% extends "base.html" %}
+{% block content %}
+ <div class="alert alert-info">
+ This is just a test deployment of Warsaw Hackerspace Vending Machine Bitcoin Payments System™.<br />
+ <b>Please report any issues to <a href="mailto:informatic@hackerspace.pl" class="alert-link">informatic@hackerspace.pl</a>.</b>
+ </div>
+ <div class="row">
+ {% for item in items %}
+ <div class="col-sm-6">
+ <div class="well vend-item">
<div class="row">
- {% for item in items %}
- <div class="col-sm-6">
- <div class="well vend-item">
- <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 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 text-center">
+ {% 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"/>
+ <code><small>{{ config['INPUT_ADDRESS'] }}</small></code>
+ </a>
+ {% endwith %}
</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>
+ </div>
+ {% endfor %}
+ </div>
+{% endblock %}
diff --git a/templates/log.html b/templates/log.html
new file mode 100644
index 0000000..d2c6e7c
--- /dev/null
+++ b/templates/log.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="row">
+ <table class="table">
+ <thead>
+ <tr><th>Hash</th><th>Date</th><th>Product</th><th>Value / Product value</th></tr>
+ </thead>
+ {% for tx in transactions %}
+ <tr>
+ <td>{{ tx.tx_hash }}</td><td>{{ tx.created }}</td><td>{{ tx.product_id }}</td><td>{{ tx.value }} / {{ tx.product_value }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+</div>
+{% endblock %}