*: reformat using black
parent
c888873b7a
commit
d5896d5d16
168
accept.py
168
accept.py
|
@ -5,127 +5,129 @@ import threading
|
||||||
import pprint
|
import pprint
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
INPUT_ADDR = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7' # bitvend addr
|
INPUT_ADDR = "12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7" # bitvend addr
|
||||||
#INPUT_ADDR = '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22' # test addr
|
# INPUT_ADDR = '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22' # test addr
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_rate(currency="PLN"):
|
||||||
|
return requests.get("https://blockchain.info/pl/ticker").json()[currency]["last"]
|
||||||
|
|
||||||
def get_exchange_rate(currency='PLN'):
|
|
||||||
return requests.get('https://blockchain.info/pl/ticker').json()[currency]['last']
|
|
||||||
|
|
||||||
def to_local_currency(sat):
|
def to_local_currency(sat):
|
||||||
# Returns satoshi in local lowest denomination currency (grosze)
|
# Returns satoshi in local lowest denomination currency (grosze)
|
||||||
rate = get_exchange_rate()
|
rate = get_exchange_rate()
|
||||||
return int(sat / 1000000.0 * rate)
|
return int(sat / 1000000.0 * rate)
|
||||||
|
|
||||||
|
|
||||||
def process_transaction(tx):
|
def process_transaction(tx):
|
||||||
tx_size = tx['x']['size']
|
tx_size = tx["x"]["size"]
|
||||||
tx_hash = tx['x']['hash']
|
tx_hash = tx["x"]["hash"]
|
||||||
tx_value = sum([
|
tx_value = sum([o["value"] for o in tx["x"]["out"] if o["addr"] == INPUT_ADDR], 0)
|
||||||
o['value'] for o in tx['x']['out'] if o['addr'] == INPUT_ADDR
|
fee = sum([i["prev_out"]["value"] for i in tx["x"]["inputs"]]) - sum(
|
||||||
], 0)
|
[o["value"] for o in tx["x"]["out"]]
|
||||||
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
|
fee_byte = fee / tx_size
|
||||||
|
|
||||||
print(tx_size, tx_hash, tx_value, fee, fee_byte)
|
print(tx_size, tx_hash, tx_value, fee, fee_byte)
|
||||||
print(to_local_currency(tx_value))
|
print(to_local_currency(tx_value))
|
||||||
|
|
||||||
|
|
||||||
def on_message(ws, message):
|
def on_message(ws, message):
|
||||||
#print message
|
# print message
|
||||||
data = json.loads(message)
|
data = json.loads(message)
|
||||||
if data['op'] == 'utx':
|
if data["op"] == "utx":
|
||||||
process_transaction(data)
|
process_transaction(data)
|
||||||
pprint.pprint(data)
|
pprint.pprint(data)
|
||||||
for d in data['x']['out']:
|
for d in data["x"]["out"]:
|
||||||
pprint.pprint(d)
|
pprint.pprint(d)
|
||||||
|
|
||||||
|
|
||||||
def on_error(ws, error):
|
def on_error(ws, error):
|
||||||
print(error)
|
print(error)
|
||||||
|
|
||||||
|
|
||||||
def on_close(ws):
|
def on_close(ws):
|
||||||
print("### closed ###")
|
print("### closed ###")
|
||||||
|
|
||||||
|
|
||||||
def on_open(ws):
|
def on_open(ws):
|
||||||
print("### connected ###")
|
print("### connected ###")
|
||||||
ws.send(json.dumps({
|
ws.send(json.dumps({"op": "addr_sub", "addr": INPUT_ADDR}))
|
||||||
"op": "addr_sub",
|
ws.send(
|
||||||
"addr": INPUT_ADDR
|
json.dumps({"op": "addr_sub", "addr": "1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22"})
|
||||||
}))
|
)
|
||||||
ws.send(json.dumps({
|
|
||||||
"op": "addr_sub",
|
|
||||||
"addr": "1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22"
|
|
||||||
}))
|
|
||||||
|
|
||||||
def run(*args):
|
def run(*args):
|
||||||
while True:
|
while True:
|
||||||
time.sleep(20)
|
time.sleep(20)
|
||||||
ws.send(json.dumps({
|
ws.send(json.dumps({"op": "ping"}))
|
||||||
"op": "ping"
|
|
||||||
}))
|
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
'''
|
"""
|
||||||
process_transaction({'op': 'utx',
|
process_transaction({'op': 'utx',
|
||||||
'x': {'hash': '03ed0015c29dfc3bfd3d8a215490d82e3562fe4e8bf5f2ffa737ac8fdc850cc4',
|
'x': {'hash': '03ed0015c29dfc3bfd3d8a215490d82e3562fe4e8bf5f2ffa737ac8fdc850cc4',
|
||||||
'inputs': [{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
'inputs': [{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
||||||
'n': 0,
|
'n': 0,
|
||||||
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
||||||
'spent': False,
|
'spent': False,
|
||||||
'tx_index': 93845150,
|
'tx_index': 93845150,
|
||||||
'type': 0,
|
'type': 0,
|
||||||
'value': 300000},
|
'value': 300000},
|
||||||
'script': '483045022100980b532c7b417b6a7ce4ef1b5e509dda61762ccbbfd35189c2ed7985e6a35d1c02205afacbda7b0e10c6b41100ecee5fc05ab47ca00c85b842923324d55c0db529f8014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
|
'script': '483045022100980b532c7b417b6a7ce4ef1b5e509dda61762ccbbfd35189c2ed7985e6a35d1c02205afacbda7b0e10c6b41100ecee5fc05ab47ca00c85b842923324d55c0db529f8014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
|
||||||
'sequence': 4294967295},
|
'sequence': 4294967295},
|
||||||
{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
||||||
'n': 0,
|
'n': 0,
|
||||||
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
||||||
'spent': False,
|
'spent': False,
|
||||||
'tx_index': 182282259,
|
'tx_index': 182282259,
|
||||||
'type': 0,
|
'type': 0,
|
||||||
'value': 2978800},
|
'value': 2978800},
|
||||||
'script': '483045022100a560606252d83635c784139ae271db498e6528d561105d72c892dc80369036d3022048bfe431f3055d115f8faf0bf2b582912fa226a2f871b44f8bef348a278c8cea014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
|
'script': '483045022100a560606252d83635c784139ae271db498e6528d561105d72c892dc80369036d3022048bfe431f3055d115f8faf0bf2b582912fa226a2f871b44f8bef348a278c8cea014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
|
||||||
'sequence': 4294967295},
|
'sequence': 4294967295},
|
||||||
{'prev_out': {'addr': '17qC9bmMCyaReEPRWksYt99tr8s8YWLrDK',
|
{'prev_out': {'addr': '17qC9bmMCyaReEPRWksYt99tr8s8YWLrDK',
|
||||||
'n': 0,
|
'n': 0,
|
||||||
'script': '76a9144aee06e79fe2f92c3916b0cc0a78478e2434680e88ac',
|
'script': '76a9144aee06e79fe2f92c3916b0cc0a78478e2434680e88ac',
|
||||||
'spent': False,
|
'spent': False,
|
||||||
'tx_index': 93651246,
|
'tx_index': 93651246,
|
||||||
'type': 0,
|
'type': 0,
|
||||||
'value': 407500},
|
'value': 407500},
|
||||||
'script': '47304402207b0946f973566ec65f4fb3878285274410e10b758806d8b46407d5219ffb36a1022017fdde92072e94bde68271754000636aa94952b91c2d818ed6d88a08cbf900fb0121029f8261faed04d668a78eadf7a3ef845de409e500dace54e604c547702a67b4fc',
|
'script': '47304402207b0946f973566ec65f4fb3878285274410e10b758806d8b46407d5219ffb36a1022017fdde92072e94bde68271754000636aa94952b91c2d818ed6d88a08cbf900fb0121029f8261faed04d668a78eadf7a3ef845de409e500dace54e604c547702a67b4fc',
|
||||||
'sequence': 4294967295}],
|
'sequence': 4294967295}],
|
||||||
'lock_time': 0,
|
'lock_time': 0,
|
||||||
'out': [{'addr': '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22',
|
'out': [{'addr': '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22',
|
||||||
'n': 0,
|
'n': 0,
|
||||||
'script': '76a914e17465c3ef40d44508c7e923140d54c3cd8ced3d88ac',
|
'script': '76a914e17465c3ef40d44508c7e923140d54c3cd8ced3d88ac',
|
||||||
'spent': True,
|
'spent': True,
|
||||||
'tx_index': 209616370,
|
'tx_index': 209616370,
|
||||||
'type': 0,
|
'type': 0,
|
||||||
'value': 1251574},
|
'value': 1251574},
|
||||||
{'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
{'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
||||||
'n': 1,
|
'n': 1,
|
||||||
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
|
||||||
'spent': True,
|
'spent': True,
|
||||||
'tx_index': 209616370,
|
'tx_index': 209616370,
|
||||||
'type': 0,
|
'type': 0,
|
||||||
'value': 2424726}],
|
'value': 2424726}],
|
||||||
'relayed_by': '5.189.53.123',
|
'relayed_by': '5.189.53.123',
|
||||||
'size': 585,
|
'size': 585,
|
||||||
'time': 1484319619,
|
'time': 1484319619,
|
||||||
'tx_index': 209616370,
|
'tx_index': 209616370,
|
||||||
'ver': 1,
|
'ver': 1,
|
||||||
'vin_sz': 3,
|
'vin_sz': 3,
|
||||||
'vout_sz': 2}})
|
'vout_sz': 2}})
|
||||||
exit(0)
|
exit(0)
|
||||||
'''
|
"""
|
||||||
websocket.enableTrace(True)
|
websocket.enableTrace(True)
|
||||||
ws = websocket.WebSocketApp("wss://ws.blockchain.info/inv",
|
ws = websocket.WebSocketApp(
|
||||||
on_message = on_message,
|
"wss://ws.blockchain.info/inv",
|
||||||
on_error = on_error,
|
on_message=on_message,
|
||||||
on_close = on_close)
|
on_error=on_error,
|
||||||
|
on_close=on_close,
|
||||||
|
)
|
||||||
ws.on_open = on_open
|
ws.on_open = on_open
|
||||||
print("### running... ###")
|
print("### running... ###")
|
||||||
ws.run_forever()
|
ws.run_forever()
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG) # noqa
|
logging.basicConfig(level=logging.DEBUG) # noqa
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
from bitvend import create_app, dev, proc, db
|
from bitvend import create_app, dev, proc, db
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from prometheus_client import start_http_server
|
from prometheus_client import start_http_server
|
||||||
|
|
||||||
start_http_server(8000)
|
start_http_server(8000)
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
@ -16,8 +17,12 @@ if __name__ == "__main__":
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
threading.Thread(target=app.run, kwargs={
|
threading.Thread(
|
||||||
'host': '0.0.0.0',
|
target=app.run,
|
||||||
}, daemon=True).start()
|
kwargs={
|
||||||
#proc.start()
|
"host": "0.0.0.0",
|
||||||
|
},
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
# proc.start()
|
||||||
dev.run()
|
dev.run()
|
||||||
|
|
|
@ -9,13 +9,13 @@ dev = BitvendCashlessMDBDevice()
|
||||||
proc = PaymentProcessor(dev)
|
proc = PaymentProcessor(dev)
|
||||||
spaceauth = SpaceAuth()
|
spaceauth = SpaceAuth()
|
||||||
|
|
||||||
from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
|
from bitvend.utils import to_local_currency, from_local_currency, format_btc, sat_to_btc
|
||||||
sat_to_btc
|
|
||||||
from bitvend.models import db, Transaction, User
|
from bitvend.models import db, Transaction, User
|
||||||
|
|
||||||
import bitvend.views
|
import bitvend.views
|
||||||
import bitvend.admin
|
import bitvend.admin
|
||||||
|
|
||||||
|
|
||||||
@spaceauth.user_loader
|
@spaceauth.user_loader
|
||||||
def bitvend_user_loader(username, profile=None):
|
def bitvend_user_loader(username, profile=None):
|
||||||
u = User.find(username)
|
u = User.find(username)
|
||||||
|
@ -27,15 +27,21 @@ def bitvend_user_loader(username, profile=None):
|
||||||
|
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.config.from_object('bitvend.default_settings')
|
app.config.from_object("bitvend.default_settings")
|
||||||
print('Loading extra settings from {}...'.format(os.environ.get('BITVEND_SETTINGS', '')))
|
print(
|
||||||
app.config.from_pyfile(os.environ.get('BITVEND_SETTINGS', ''), silent=True)
|
"Loading extra settings from {}...".format(
|
||||||
|
os.environ.get("BITVEND_SETTINGS", "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app.config.from_pyfile(os.environ.get("BITVEND_SETTINGS", ""), silent=True)
|
||||||
|
|
||||||
# Use proper proxy headers, this fixes invalid scheme in
|
# Use proper proxy headers, this fixes invalid scheme in
|
||||||
# url_for(_external=True)
|
# url_for(_external=True)
|
||||||
from werkzeug.contrib.fixers import ProxyFix
|
from werkzeug.contrib.fixers import ProxyFix
|
||||||
|
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
@ -44,27 +50,31 @@ def create_app():
|
||||||
proc.init_app(app)
|
proc.init_app(app)
|
||||||
|
|
||||||
app.register_blueprint(bitvend.views.bp)
|
app.register_blueprint(bitvend.views.bp)
|
||||||
app.register_blueprint(bitvend.admin.bp, url_prefix='/admin')
|
app.register_blueprint(bitvend.admin.bp, url_prefix="/admin")
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def ctx_utils():
|
def ctx_utils():
|
||||||
return {
|
return {
|
||||||
'from_local_currency': from_local_currency,
|
"from_local_currency": from_local_currency,
|
||||||
'to_local_currency': to_local_currency,
|
"to_local_currency": to_local_currency,
|
||||||
'format_btc': format_btc,
|
"format_btc": format_btc,
|
||||||
'sat_to_btc': sat_to_btc,
|
"sat_to_btc": sat_to_btc,
|
||||||
'qrcode': lambda data: flask.url_for('bitvend.qrcode_gen', data=data),
|
"qrcode": lambda data: flask.url_for("bitvend.qrcode_gen", data=data),
|
||||||
'current_transaction': Transaction.query.filter(Transaction.finished == False).first(),
|
"current_transaction": Transaction.query.filter(
|
||||||
'mdb_online': dev.online,
|
Transaction.finished == False
|
||||||
'proc_online': proc.online,
|
).first(),
|
||||||
'static': lambda fn, **kwargs: flask.url_for('static', filename=fn,
|
"mdb_online": dev.online,
|
||||||
**kwargs)
|
"proc_online": proc.online,
|
||||||
|
"static": lambda fn, **kwargs: flask.url_for(
|
||||||
|
"static", filename=fn, **kwargs
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def url_for_other_page(page):
|
def url_for_other_page(page):
|
||||||
args = flask.request.view_args.copy()
|
args = flask.request.view_args.copy()
|
||||||
args['page'] = page
|
args["page"] = page
|
||||||
return flask.url_for(flask.request.endpoint, **args)
|
return flask.url_for(flask.request.endpoint, **args)
|
||||||
app.jinja_env.globals['url_for_other_page'] = url_for_other_page
|
|
||||||
|
app.jinja_env.globals["url_for_other_page"] = url_for_other_page
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -7,46 +7,46 @@ from bitvend.forms import ManualForm
|
||||||
from spaceauth import cap_required
|
from spaceauth import cap_required
|
||||||
|
|
||||||
|
|
||||||
admin_required = cap_required('staff')
|
admin_required = cap_required("staff")
|
||||||
bp = Blueprint('admin', __name__)
|
bp = Blueprint("admin", __name__)
|
||||||
|
|
||||||
@bp.route('/manual', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/manual", methods=["GET", "POST"])
|
||||||
@fresh_login_required
|
@fresh_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def manual():
|
def manual():
|
||||||
form = ManualForm()
|
form = ManualForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
current_user.transactions.append(Transaction(
|
current_user.transactions.append(Transaction(amount=form.amount.data))
|
||||||
amount=form.amount.data
|
|
||||||
))
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Operation successful.', 'success')
|
flash("Operation successful.", "success")
|
||||||
|
|
||||||
return render_template('admin/manual.html', form=form)
|
return render_template("admin/manual.html", form=form)
|
||||||
|
|
||||||
@bp.route('/transactions/', defaults={'page': 1})
|
|
||||||
@bp.route('/transactions/p/<int:page>')
|
@bp.route("/transactions/", defaults={"page": 1})
|
||||||
|
@bp.route("/transactions/p/<int:page>")
|
||||||
@fresh_login_required
|
@fresh_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def transactions(page):
|
def transactions(page):
|
||||||
return render_template('admin/transactions.html',
|
return render_template(
|
||||||
transactions=Transaction.query.paginate(page)
|
"admin/transactions.html", transactions=Transaction.query.paginate(page)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/begin')
|
@bp.route("/begin")
|
||||||
@fresh_login_required
|
@fresh_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def begin():
|
def begin():
|
||||||
dev.begin_session(500)
|
dev.begin_session(500)
|
||||||
flash('Operation successful.', 'success')
|
flash("Operation successful.", "success")
|
||||||
return redirect('/')
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/cancel')
|
@bp.route("/cancel")
|
||||||
@fresh_login_required
|
@fresh_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def cancel():
|
def cancel():
|
||||||
dev.cancel_session()
|
dev.cancel_session()
|
||||||
flash('Operation successful.', 'success')
|
flash("Operation successful.", "success")
|
||||||
return redirect('/')
|
return redirect("/")
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
SECRET_KEY = 'testing'
|
SECRET_KEY = "testing"
|
||||||
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///storage-%s.db' % (platform.node(),)
|
SQLALCHEMY_DATABASE_URI = "sqlite:///storage-%s.db" % (platform.node(),)
|
||||||
|
|
||||||
INPUT_ADDRESS = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'
|
INPUT_ADDRESS = "12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7"
|
||||||
BLOCKCYPHER_CHAIN = 'btc/main'
|
BLOCKCYPHER_CHAIN = "btc/main"
|
||||||
|
|
||||||
#INPUT_ADDRESS ='n2SYFMbgfG4LXkvB4VgF4SeSEqQE28NAuV'
|
# INPUT_ADDRESS ='n2SYFMbgfG4LXkvB4VgF4SeSEqQE28NAuV'
|
||||||
#BLOCKCYPHER_CHAIN = 'btc/test3'
|
# BLOCKCYPHER_CHAIN = 'btc/test3'
|
||||||
|
|
||||||
BLOCKCYPHER_TOKEN = '918ddf2a06184ec295ca1cb636db20b5'
|
BLOCKCYPHER_TOKEN = "918ddf2a06184ec295ca1cb636db20b5"
|
||||||
|
|
||||||
TEMPLATES_AUTO_RELOAD = True
|
TEMPLATES_AUTO_RELOAD = True
|
||||||
ITEMS = [
|
ITEMS = [
|
||||||
{
|
{
|
||||||
'name': 'Club Mate',
|
"name": "Club Mate",
|
||||||
'image': '/static/img/club-mate.png',
|
"image": "/static/img/club-mate.png",
|
||||||
'value': 500,
|
"value": 500,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Mate Mate',
|
"name": "Mate Mate",
|
||||||
'image': '/static/img/mate-mate.png',
|
"image": "/static/img/mate-mate.png",
|
||||||
'value': 600,
|
"value": 600,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Arduino Pro Micro',
|
"name": "Arduino Pro Micro",
|
||||||
'image': '/static/img/promicro.png',
|
"image": "/static/img/promicro.png",
|
||||||
'value': 1600,
|
"value": 1600,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Arduino Pro Mini',
|
"name": "Arduino Pro Mini",
|
||||||
'image': '/static/img/promini.png',
|
"image": "/static/img/promini.png",
|
||||||
'value': 750,
|
"value": 750,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'NodeMCU (ESP8266)',
|
"name": "NodeMCU (ESP8266)",
|
||||||
'image': '/static/img/nodemcu.png',
|
"image": "/static/img/nodemcu.png",
|
||||||
'value': 1700,
|
"value": 1700,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ class DecimalUnityField(DecimalField):
|
||||||
|
|
||||||
def _value(self):
|
def _value(self):
|
||||||
if self.data is not None:
|
if self.data is not None:
|
||||||
format = '%%0.%df' % self.places
|
format = "%%0.%df" % self.places
|
||||||
return (format % (Decimal(self.data) / self.unity,))
|
return format % (Decimal(self.data) / self.unity,)
|
||||||
elif self.raw_data:
|
elif self.raw_data:
|
||||||
return self.raw_data[0]
|
return self.raw_data[0]
|
||||||
|
|
||||||
|
@ -28,21 +28,29 @@ class DecimalUnityField(DecimalField):
|
||||||
except InvalidOperation:
|
except InvalidOperation:
|
||||||
self.data = None
|
self.data = None
|
||||||
|
|
||||||
|
|
||||||
def UserExists(form, field):
|
def UserExists(form, field):
|
||||||
if not User.query.get(field.data):
|
if not User.query.get(field.data):
|
||||||
raise ValidationError('User does not exist.')
|
raise ValidationError("User does not exist.")
|
||||||
|
|
||||||
|
|
||||||
def NotCurrentUser(form, field):
|
def NotCurrentUser(form, field):
|
||||||
if field.data == current_user.uid:
|
if field.data == current_user.uid:
|
||||||
raise ValidationError('Are you serious?')
|
raise ValidationError("Are you serious?")
|
||||||
|
|
||||||
|
|
||||||
class TransferForm(FlaskForm):
|
class TransferForm(FlaskForm):
|
||||||
target = StringField("Target user", validators=[
|
target = StringField(
|
||||||
DataRequired(), UserExists, NotCurrentUser])
|
"Target user", validators=[DataRequired(), UserExists, NotCurrentUser]
|
||||||
amount = DecimalUnityField("Amount", default=0, validators=[
|
)
|
||||||
NumberRange(min=1),
|
amount = DecimalUnityField(
|
||||||
])
|
"Amount",
|
||||||
|
default=0,
|
||||||
|
validators=[
|
||||||
|
NumberRange(min=1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManualForm(FlaskForm):
|
class ManualForm(FlaskForm):
|
||||||
amount = DecimalUnityField("Amount", default=0, validators=[
|
amount = DecimalUnityField("Amount", default=0, validators=[])
|
||||||
])
|
|
||||||
|
|
|
@ -6,12 +6,14 @@ from bitvend.models import db, Transaction
|
||||||
|
|
||||||
|
|
||||||
def daterange(start_date, end_date):
|
def daterange(start_date, end_date):
|
||||||
for n in range(int((end_date - start_date).days)+1):
|
for n in range(int((end_date - start_date).days) + 1):
|
||||||
yield (start_date + timedelta(n)).date()
|
yield (start_date + timedelta(n)).date()
|
||||||
|
|
||||||
|
|
||||||
def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
|
def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
|
||||||
date_hash = {datetime.strptime(xdate, '%Y-%m-%d').date(): count for xdate, count in resultset}
|
date_hash = {
|
||||||
|
datetime.strptime(xdate, "%Y-%m-%d").date(): count for xdate, count in resultset
|
||||||
|
}
|
||||||
|
|
||||||
if not date_from:
|
if not date_from:
|
||||||
date_from = min(date_hash.keys())
|
date_from = min(date_hash.keys())
|
||||||
|
@ -22,18 +24,16 @@ def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
|
||||||
return [
|
return [
|
||||||
type(default)(date_hash[d]) if d in date_hash else default
|
type(default)(date_hash[d]) if d in date_hash else default
|
||||||
for d in daterange(date_from, date_to)
|
for d in daterange(date_from, date_to)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def gen_database_graph(title, query, date_from, date_to, default=0):
|
def gen_database_graph(title, query, date_from, date_to, default=0):
|
||||||
date_column = query.column_descriptions[0]['expr']
|
date_column = query.column_descriptions[0]["expr"]
|
||||||
resultset = query \
|
resultset = (
|
||||||
.group_by(
|
query.group_by(date_column)
|
||||||
date_column
|
.filter(date_column >= date_from, date_column <= date_to)
|
||||||
).filter(
|
.all()
|
||||||
date_column >= date_from,
|
)
|
||||||
date_column <= date_to
|
|
||||||
).all()
|
|
||||||
|
|
||||||
return [title] + gen_graph_dataset(resultset, date_from, date_to, default)
|
return [title] + gen_graph_dataset(resultset, date_from, date_to, default)
|
||||||
|
|
||||||
|
@ -55,13 +55,18 @@ def gen_main_graph(date_from=None, date_to=None):
|
||||||
date_from = datetime.combine(date_from, datetime.min.time())
|
date_from = datetime.combine(date_from, datetime.min.time())
|
||||||
date_to = datetime.combine(date_to, datetime.max.time())
|
date_to = datetime.combine(date_to, datetime.max.time())
|
||||||
|
|
||||||
return zip_graph([
|
return zip_graph(
|
||||||
['date'] + [n.strftime('%Y-%m-%d')
|
[
|
||||||
for n in daterange(date_from, date_to)],
|
["date"] + [n.strftime("%Y-%m-%d") for n in daterange(date_from, date_to)],
|
||||||
|
gen_database_graph(
|
||||||
gen_database_graph('purchases', db.session.query(
|
"purchases",
|
||||||
#func.date_trunc('day', Transaction.created),
|
db.session.query(
|
||||||
func.strftime('%Y-%m-%d', Transaction.created),
|
# func.date_trunc('day', Transaction.created),
|
||||||
func.count(Transaction.created),
|
func.strftime("%Y-%m-%d", Transaction.created),
|
||||||
).filter(Transaction.type == 'purchase'), date_from, date_to),
|
func.count(Transaction.created),
|
||||||
])
|
).filter(Transaction.type == "purchase"),
|
||||||
|
date_from,
|
||||||
|
date_to,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -22,11 +22,11 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
|
||||||
def vend_request(self, product, value):
|
def vend_request(self, product, value):
|
||||||
# FIXME we report success here, because database write takes too much
|
# FIXME we report success here, because database write takes too much
|
||||||
# time to respond in 5ms.
|
# time to respond in 5ms.
|
||||||
self.send([0x05, 0x00, 0xff])
|
self.send([0x05, 0x00, 0xFF])
|
||||||
self.poll_queue.put([0x05])
|
self.poll_queue.put([0x05])
|
||||||
self.current_request.processed = True
|
self.current_request.processed = True
|
||||||
|
|
||||||
self.logger.info('got vend request: %r', self.current_tx_id)
|
self.logger.info("got vend request: %r", self.current_tx_id)
|
||||||
|
|
||||||
if self.current_tx_id:
|
if self.current_tx_id:
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
|
@ -48,20 +48,30 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
|
||||||
last_purchase = 0
|
last_purchase = 0
|
||||||
|
|
||||||
def process_request(self, req):
|
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:
|
if (
|
||||||
self.logger.info('Purchase with change detected')
|
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
|
req.processed = True
|
||||||
if time.time() - self.last_purchase > 5:
|
if time.time() - self.last_purchase > 5:
|
||||||
purchase_counter.inc()
|
purchase_counter.inc()
|
||||||
self.last_purchase = time.time()
|
self.last_purchase = time.time()
|
||||||
elif req.command == COIN_TYPE and req.validate_checksum() and not req.processed:
|
elif req.command == COIN_TYPE and req.validate_checksum() and not req.processed:
|
||||||
self.logger.info('Purchase without detected')
|
self.logger.info("Purchase without detected")
|
||||||
req.processed = True
|
req.processed = True
|
||||||
if time.time() - self.last_purchase > 5:
|
if time.time() - self.last_purchase > 5:
|
||||||
purchase_counter.inc()
|
purchase_counter.inc()
|
||||||
self.last_purchase = time.time()
|
self.last_purchase = time.time()
|
||||||
elif req.command == COIN_POLL and req.validate_checksum() and not req.processed and req.ack:
|
elif (
|
||||||
self.logger.info('Coin detected')
|
req.command == COIN_POLL
|
||||||
|
and req.validate_checksum()
|
||||||
|
and not req.processed
|
||||||
|
and req.ack
|
||||||
|
):
|
||||||
|
self.logger.info("Coin detected")
|
||||||
req.processed = True
|
req.processed = True
|
||||||
coin_counter.inc()
|
coin_counter.inc()
|
||||||
|
|
||||||
|
|
|
@ -7,21 +7,26 @@ from sqlalchemy.sql import func, select, and_
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
class TransferException(Exception): pass
|
|
||||||
class NoFunds(TransferException): pass
|
class TransferException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoFunds(TransferException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
class User(db.Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = "users"
|
||||||
|
|
||||||
uid = db.Column(db.String(64), primary_key=True)
|
uid = db.Column(db.String(64), primary_key=True)
|
||||||
transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
|
transactions = db.relationship("Transaction", backref="user", lazy="dynamic")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.uid
|
return self.uid
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<User {0.uid} {0.balance}>'.format(self)
|
return "<User {0.uid} {0.balance}>".format(self)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def balance(self):
|
def balance(self):
|
||||||
|
@ -29,51 +34,53 @@ class User(db.Model):
|
||||||
|
|
||||||
@balance.expression
|
@balance.expression
|
||||||
def balance(self):
|
def balance(self):
|
||||||
return (select([func.sum(Transaction.amount)]).
|
return (
|
||||||
where(Transaction.uid == User.uid).
|
select([func.sum(Transaction.amount)])
|
||||||
label("balance")
|
.where(Transaction.uid == User.uid)
|
||||||
)
|
.label("balance")
|
||||||
|
)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def purchase_count(self):
|
def purchase_count(self):
|
||||||
return self.transactions.filter(Transaction.type == 'purchase').count()
|
return self.transactions.filter(Transaction.type == "purchase").count()
|
||||||
|
|
||||||
@purchase_count.expression
|
@purchase_count.expression
|
||||||
def purchase_count(self):
|
def purchase_count(self):
|
||||||
return (select([func.count(Transaction.amount)]).
|
return (
|
||||||
where(and_(
|
select([func.count(Transaction.amount)])
|
||||||
Transaction.uid == User.uid,
|
.where(and_(Transaction.uid == User.uid, Transaction.type == "purchase"))
|
||||||
Transaction.type == 'purchase')).
|
.label("purchase_count")
|
||||||
label("purchase_count")
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def purchase_amount(self):
|
def purchase_amount(self):
|
||||||
return -sum((tx.amount or 0) for tx in self.transactions.filter(Transaction.type == 'purchase'))
|
return -sum(
|
||||||
|
(tx.amount or 0)
|
||||||
|
for tx in self.transactions.filter(Transaction.type == "purchase")
|
||||||
|
)
|
||||||
|
|
||||||
@purchase_amount.expression
|
@purchase_amount.expression
|
||||||
def purchase_amount(self):
|
def purchase_amount(self):
|
||||||
return (select([-func.sum(Transaction.amount)]).
|
return (
|
||||||
where(and_(
|
select([-func.sum(Transaction.amount)])
|
||||||
Transaction.uid == User.uid,
|
.where(and_(Transaction.uid == User.uid, Transaction.type == "purchase"))
|
||||||
Transaction.type == 'purchase')).
|
.label("purchase_amount")
|
||||||
label("purchase_amount")
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def transfer(self, target, amount):
|
def transfer(self, target, amount):
|
||||||
if amount > self.amount_available:
|
if amount > self.amount_available:
|
||||||
raise NoFunds()
|
raise NoFunds()
|
||||||
|
|
||||||
self.transactions.append(Transaction(
|
self.transactions.append(
|
||||||
amount=-amount, type='transfer', related=target.uid
|
Transaction(amount=-amount, type="transfer", related=target.uid)
|
||||||
))
|
)
|
||||||
target.transactions.append(Transaction(
|
target.transactions.append(
|
||||||
amount=amount, type='transfer', related=self.uid
|
Transaction(amount=amount, type="transfer", related=self.uid)
|
||||||
))
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def debt_limit(self):
|
def debt_limit(self):
|
||||||
return app.config.get('DEBT_LIMIT', 5000)
|
return app.config.get("DEBT_LIMIT", 5000)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def amount_available(self):
|
def amount_available(self):
|
||||||
|
@ -96,22 +103,24 @@ class User(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Transaction(db.Model):
|
class Transaction(db.Model):
|
||||||
__tablename__ = 'transactions'
|
__tablename__ = "transactions"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
tx_hash = db.Column(db.String)
|
tx_hash = db.Column(db.String)
|
||||||
uid = db.Column(db.String(64), db.ForeignKey('users.uid'))
|
uid = db.Column(db.String(64), db.ForeignKey("users.uid"))
|
||||||
|
|
||||||
amount = db.Column(db.Integer)
|
amount = db.Column(db.Integer)
|
||||||
|
|
||||||
type = db.Column(db.String(32), default='manual')
|
type = db.Column(db.String(32), default="manual")
|
||||||
|
|
||||||
related = db.Column(db.String)
|
related = db.Column(db.String)
|
||||||
related_user = db.relationship('User', foreign_keys=[related], primaryjoin=related==User.uid)
|
related_user = db.relationship(
|
||||||
|
"User", foreign_keys=[related], primaryjoin=related == User.uid
|
||||||
|
)
|
||||||
|
|
||||||
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
#value = db.Column(db.Integer)
|
# value = db.Column(db.Integer)
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def value(self):
|
def value(self):
|
||||||
return self.amount
|
return self.amount
|
||||||
|
@ -121,11 +130,9 @@ class Transaction(db.Model):
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def finished(self):
|
def finished(self):
|
||||||
return (self.type != 'purchase') | (self.product_id != None)
|
return (self.type != "purchase") | (self.product_id != None)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"order_by": created.desc()}
|
||||||
"order_by": created.desc()
|
|
||||||
}
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Transaction {0.uid} {0.type} {0.amount} {0.created}>'.format(self)
|
return "<Transaction {0.uid} {0.type} {0.amount} {0.created}>".format(self)
|
||||||
|
|
|
@ -15,9 +15,7 @@ class PaymentProcessor(threading.Thread):
|
||||||
last_pong = 0
|
last_pong = 0
|
||||||
app = None
|
app = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, device, input_address=None, chain_id=None, app=None, token=None):
|
||||||
self, device, input_address=None, chain_id=None, app=None,
|
|
||||||
token=None):
|
|
||||||
super(PaymentProcessor, self).__init__()
|
super(PaymentProcessor, self).__init__()
|
||||||
self.device = device
|
self.device = device
|
||||||
self.input_address = input_address
|
self.input_address = input_address
|
||||||
|
@ -32,97 +30,103 @@ class PaymentProcessor(threading.Thread):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
if not self.input_address:
|
if not self.input_address:
|
||||||
self.input_address = self.app.config['INPUT_ADDRESS']
|
self.input_address = self.app.config["INPUT_ADDRESS"]
|
||||||
self.chain_id = self.app.config['BLOCKCYPHER_CHAIN']
|
self.chain_id = self.app.config["BLOCKCYPHER_CHAIN"]
|
||||||
self.token = self.app.config['BLOCKCYPHER_TOKEN']
|
self.token = self.app.config["BLOCKCYPHER_TOKEN"]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.logger.info('Starting...')
|
self.logger.info("Starting...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
ws = websocket.WebSocketApp(
|
ws = websocket.WebSocketApp(
|
||||||
"wss://socket.blockcypher.com/v1/%s?token=%s" \
|
"wss://socket.blockcypher.com/v1/%s?token=%s"
|
||||||
% (self.chain_id, self.token),
|
% (self.chain_id, self.token),
|
||||||
on_message=self.on_message,
|
on_message=self.on_message,
|
||||||
on_error=self.on_error,
|
on_error=self.on_error,
|
||||||
on_close=self.on_close,
|
on_close=self.on_close,
|
||||||
on_open=self.on_open)
|
on_open=self.on_open,
|
||||||
|
)
|
||||||
|
|
||||||
ws.run_forever(ping_timeout=20, ping_interval=30)
|
ws.run_forever(ping_timeout=20, ping_interval=30)
|
||||||
except:
|
except:
|
||||||
self.logger.exception('run_forever failed')
|
self.logger.exception("run_forever failed")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
def process_transaction(self, tx):
|
def process_transaction(self, tx):
|
||||||
tx_size = tx['size']
|
tx_size = tx["size"]
|
||||||
tx_hash = tx['hash']
|
tx_hash = tx["hash"]
|
||||||
tx_value = sum([
|
tx_value = sum(
|
||||||
o['value'] for o in tx['outputs'] if self.input_address in o['addresses']
|
[o["value"] for o in tx["outputs"] if self.input_address in o["addresses"]],
|
||||||
], 0)
|
0,
|
||||||
fee = tx['fees']
|
)
|
||||||
|
fee = tx["fees"]
|
||||||
|
|
||||||
fee_byte = fee / tx_size
|
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("%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))
|
self.logger.info("In local currency: %r", to_local_currency(tx_value))
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
intx = Transaction(
|
intx = Transaction(
|
||||||
uid='__bitcoin__',
|
uid="__bitcoin__",
|
||||||
type='bitcoin',
|
type="bitcoin",
|
||||||
amount=to_local_currency(tx_value),
|
amount=to_local_currency(tx_value),
|
||||||
tx_hash=tx_hash,
|
tx_hash=tx_hash,
|
||||||
)
|
)
|
||||||
db.session.add(intx)
|
db.session.add(intx)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
outtx = Transaction(
|
outtx = Transaction(
|
||||||
uid='__bitcoin__',
|
uid="__bitcoin__",
|
||||||
type='purchase',
|
type="purchase",
|
||||||
)
|
)
|
||||||
db.session.add(outtx)
|
db.session.add(outtx)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
tx_id = outtx.id
|
tx_id = outtx.id
|
||||||
|
|
||||||
if to_local_currency(tx_value) < 100:
|
if to_local_currency(tx_value) < 100:
|
||||||
self.logger.warning('Whyyyy so low...')
|
self.logger.warning("Whyyyy so low...")
|
||||||
return
|
return
|
||||||
|
|
||||||
if fee_byte < 15:
|
if fee_byte < 15:
|
||||||
self.logger.warning('Fee too low...')
|
self.logger.warning("Fee too low...")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info('Transaction %d ok, going to device...', tx_id)
|
self.logger.info("Transaction %d ok, going to device...", tx_id)
|
||||||
|
|
||||||
# FIXME we need better handling of ACK on POLL responses...
|
# FIXME we need better handling of ACK on POLL responses...
|
||||||
self.device.begin_session(to_local_currency(tx_value), tx_id)
|
self.device.begin_session(to_local_currency(tx_value), tx_id)
|
||||||
|
|
||||||
def on_message(self, ws, message):
|
def on_message(self, ws, message):
|
||||||
#print message
|
# print message
|
||||||
data = json.loads(message)
|
data = json.loads(message)
|
||||||
self.logger.info('msg: %r', data)
|
self.logger.info("msg: %r", data)
|
||||||
|
|
||||||
if 'inputs' in data:
|
if "inputs" in data:
|
||||||
self.process_transaction(data)
|
self.process_transaction(data)
|
||||||
elif data.get('event') == 'pong':
|
elif data.get("event") == "pong":
|
||||||
self.last_pong = time.time()
|
self.last_pong = time.time()
|
||||||
|
|
||||||
def on_error(self, ws, error):
|
def on_error(self, ws, error):
|
||||||
self.logger.error(error)
|
self.logger.error(error)
|
||||||
|
|
||||||
def on_close(self, ws):
|
def on_close(self, ws):
|
||||||
self.logger.info('Connection closed')
|
self.logger.info("Connection closed")
|
||||||
|
|
||||||
def on_open(self, ws):
|
def on_open(self, ws):
|
||||||
self.logger.info('Connected, registering for: %r', self.input_address)
|
self.logger.info("Connected, registering for: %r", self.input_address)
|
||||||
|
|
||||||
ws.send(json.dumps({
|
ws.send(
|
||||||
"event": "tx-confidence",
|
json.dumps(
|
||||||
"address": self.input_address,
|
{
|
||||||
"confidence": 0.9,
|
"event": "tx-confidence",
|
||||||
}))
|
"address": self.input_address,
|
||||||
|
"confidence": 0.9,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
threading.Thread(target=self.keepalive, args=(ws,), daemon=True).start()
|
threading.Thread(target=self.keepalive, args=(ws,), daemon=True).start()
|
||||||
|
|
||||||
|
@ -130,13 +134,11 @@ class PaymentProcessor(threading.Thread):
|
||||||
# Keepalive thread target, just send ping once in a while
|
# Keepalive thread target, just send ping once in a while
|
||||||
while True:
|
while True:
|
||||||
# FIXME check last ping time
|
# FIXME check last ping time
|
||||||
ws.send(json.dumps({
|
ws.send(json.dumps({"event": "ping"}))
|
||||||
"event": "ping"
|
|
||||||
}))
|
|
||||||
time.sleep(20)
|
time.sleep(20)
|
||||||
|
|
||||||
if time.time() - self.last_pong > 60:
|
if time.time() - self.last_pong > 60:
|
||||||
self.logger.warning('Closing socket for inactivity')
|
self.logger.warning("Closing socket for inactivity")
|
||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from prometheus_client import start_http_server, Counter
|
from prometheus_client import start_http_server, Counter
|
||||||
|
|
||||||
coin_counter = Counter('coins_inserted', 'Number of coins inserted into machine')
|
coin_counter = Counter("coins_inserted", "Number of coins inserted into machine")
|
||||||
purchase_counter = Counter('purchases', 'Number of purchases')
|
purchase_counter = Counter("purchases", "Number of purchases")
|
||||||
cashless_purchase_counter = Counter('cashless_purchases', 'Number of cashless (BTC) purchases')
|
cashless_purchase_counter = Counter(
|
||||||
|
"cashless_purchases", "Number of cashless (BTC) purchases"
|
||||||
|
)
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
import cachetools
|
import cachetools
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@cachetools.cached(cachetools.TTLCache(32, 600))
|
@cachetools.cached(cachetools.TTLCache(32, 600))
|
||||||
def get_exchange_rate(currency='PLN'):
|
def get_exchange_rate(currency="PLN"):
|
||||||
# Returns current exchange rate for selected currency
|
# Returns current exchange rate for selected currency
|
||||||
return requests.get('https://blockchain.info/pl/ticker').json()[currency]['last']
|
return requests.get("https://blockchain.info/pl/ticker").json()[currency]["last"]
|
||||||
|
|
||||||
|
|
||||||
def to_local_currency(sat, safe=False):
|
def to_local_currency(sat, safe=False):
|
||||||
# Returns satoshi in local lowest denomination currency (grosze)
|
# Returns satoshi in local lowest denomination currency (grosze)
|
||||||
|
@ -18,6 +20,7 @@ def to_local_currency(sat, safe=False):
|
||||||
raise
|
raise
|
||||||
return int(sat / 1000000.0 * rate)
|
return int(sat / 1000000.0 * rate)
|
||||||
|
|
||||||
|
|
||||||
def from_local_currency(val, safe=False):
|
def from_local_currency(val, safe=False):
|
||||||
# Returns satoshi value from local currency
|
# Returns satoshi value from local currency
|
||||||
try:
|
try:
|
||||||
|
@ -29,10 +32,12 @@ def from_local_currency(val, safe=False):
|
||||||
|
|
||||||
return int(val / rate * 1000000)
|
return int(val / rate * 1000000)
|
||||||
|
|
||||||
|
|
||||||
def sat_to_btc(amount):
|
def sat_to_btc(amount):
|
||||||
# Converts satoshi to BTC
|
# Converts satoshi to BTC
|
||||||
return amount / 100000000.0
|
return amount / 100000000.0
|
||||||
|
|
||||||
|
|
||||||
def format_btc(amount):
|
def format_btc(amount):
|
||||||
# Formats satoshi to human-readable format
|
# Formats satoshi to human-readable format
|
||||||
return (u'฿%.8f' % (sat_to_btc(amount),)).rstrip('0').rstrip('.')
|
return ("฿%.8f" % (sat_to_btc(amount),)).rstrip("0").rstrip(".")
|
||||||
|
|
120
bitvend/views.py
120
bitvend/views.py
|
@ -1,5 +1,4 @@
|
||||||
from flask import Blueprint, render_template, redirect, request, flash, \
|
from flask import Blueprint, render_template, redirect, request, flash, url_for, jsonify
|
||||||
url_for, jsonify
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -13,92 +12,103 @@ from bitvend.graphs import gen_main_graph
|
||||||
|
|
||||||
from spaceauth import login_required, current_user, cap_required
|
from spaceauth import login_required, current_user, cap_required
|
||||||
|
|
||||||
bp = Blueprint('bitvend', __name__, template_folder='templates')
|
bp = Blueprint("bitvend", __name__, template_folder="templates")
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
|
@bp.route("/")
|
||||||
def index():
|
def index():
|
||||||
transactions = []
|
transactions = []
|
||||||
hallofshame = User.query \
|
hallofshame = (
|
||||||
.with_entities(User, User.balance) \
|
User.query.with_entities(User, User.balance)
|
||||||
.order_by(User.balance.asc()) \
|
.order_by(User.balance.asc())
|
||||||
.filter(User.balance < 0) \
|
.filter(User.balance < 0)
|
||||||
.limit(5) \
|
.limit(5)
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
hallofaddicts = User.query \
|
hallofaddicts = (
|
||||||
.with_entities(User, User.purchase_amount, User.purchase_count) \
|
User.query.with_entities(User, User.purchase_amount, User.purchase_count)
|
||||||
.order_by(User.purchase_amount.desc()) \
|
.order_by(User.purchase_amount.desc())
|
||||||
.filter(User.purchase_amount > 0) \
|
.filter(User.purchase_amount > 0)
|
||||||
.limit(5) \
|
.limit(5)
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
bottles_purchased = Transaction.query \
|
bottles_purchased = Transaction.query.filter(
|
||||||
.filter(Transaction.amount.in_([-500, -600]), Transaction.type == 'purchase') \
|
Transaction.amount.in_([-500, -600]), Transaction.type == "purchase"
|
||||||
.count()
|
).count()
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
transactions = current_user.transactions.order_by(Transaction.created.desc()).limit(10)
|
transactions = current_user.transactions.order_by(
|
||||||
|
Transaction.created.desc()
|
||||||
|
).limit(10)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'index.html',
|
"index.html",
|
||||||
items=app.config['ITEMS'],
|
items=app.config["ITEMS"],
|
||||||
transactions=transactions,
|
transactions=transactions,
|
||||||
transfer_form=TransferForm(),
|
transfer_form=TransferForm(),
|
||||||
hallofshame=hallofshame,
|
hallofshame=hallofshame,
|
||||||
hallofaddicts=hallofaddicts,
|
hallofaddicts=hallofaddicts,
|
||||||
bottles_purchased=bottles_purchased,
|
bottles_purchased=bottles_purchased,
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/transactions/', defaults={'page': 1})
|
|
||||||
@bp.route('/transactions/p/<int:page>')
|
@bp.route("/transactions/", defaults={"page": 1})
|
||||||
|
@bp.route("/transactions/p/<int:page>")
|
||||||
def transactions(page):
|
def transactions(page):
|
||||||
return render_template('transactions.html',
|
return render_template(
|
||||||
transactions=current_user.transactions.paginate(page)
|
"transactions.html", transactions=current_user.transactions.paginate(page)
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/transfer', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/transfer", methods=["GET", "POST"])
|
||||||
def transfer():
|
def transfer():
|
||||||
transfer_form = TransferForm()
|
transfer_form = TransferForm()
|
||||||
|
|
||||||
if transfer_form.validate_on_submit():
|
if transfer_form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
current_user.transfer(User.query.get(transfer_form.target.data), transfer_form.amount.data)
|
current_user.transfer(
|
||||||
|
User.query.get(transfer_form.target.data), transfer_form.amount.data
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Transfer succeeded.', 'success')
|
flash("Transfer succeeded.", "success")
|
||||||
except NoFunds:
|
except NoFunds:
|
||||||
flash('No funds.', 'danger')
|
flash("No funds.", "danger")
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
flash('; '.join(sum(transfer_form.errors.values(), [])), 'danger')
|
flash("; ".join(sum(transfer_form.errors.values(), [])), "danger")
|
||||||
|
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
@bp.route('/log')
|
|
||||||
|
@bp.route("/log")
|
||||||
@login_required
|
@login_required
|
||||||
@cap_required('staff')
|
@cap_required("staff")
|
||||||
def log():
|
def log():
|
||||||
return render_template(
|
return render_template("log.html", transactions=Transaction.query.all())
|
||||||
'log.html', transactions=Transaction.query.all())
|
|
||||||
|
|
||||||
@bp.route('/begin')
|
|
||||||
|
@bp.route("/begin")
|
||||||
@login_required
|
@login_required
|
||||||
def begin():
|
def begin():
|
||||||
if Transaction.query.filter(Transaction.finished == False).count():
|
if Transaction.query.filter(Transaction.finished == False).count():
|
||||||
flash('Nope xD', 'danger')
|
flash("Nope xD", "danger")
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
if current_user.amount_available <= 0:
|
if current_user.amount_available <= 0:
|
||||||
flash('Nope xD', 'danger')
|
flash("Nope xD", "danger")
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
tx = Transaction(type='purchase')
|
tx = Transaction(type="purchase")
|
||||||
current_user.transactions.append(tx)
|
current_user.transactions.append(tx)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
dev.begin_session(current_user.amount_available, tx.id)
|
dev.begin_session(current_user.amount_available, tx.id)
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
@bp.route('/cancel')
|
|
||||||
|
@bp.route("/cancel")
|
||||||
@login_required
|
@login_required
|
||||||
def cancel():
|
def cancel():
|
||||||
dev.cancel_session()
|
dev.cancel_session()
|
||||||
|
@ -108,20 +118,26 @@ def cancel():
|
||||||
Transaction.query.filter(Transaction.finished == False).delete()
|
Transaction.query.filter(Transaction.finished == False).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for(".index"))
|
||||||
|
|
||||||
@bp.route('/qrcode/<path:data>')
|
|
||||||
|
@bp.route("/qrcode/<path:data>")
|
||||||
def qrcode_gen(data):
|
def qrcode_gen(data):
|
||||||
bio = six.BytesIO()
|
bio = six.BytesIO()
|
||||||
qr = qrcode.QRCode(border=0, box_size=50)
|
qr = qrcode.QRCode(border=0, box_size=50)
|
||||||
qr.add_data(data)
|
qr.add_data(data)
|
||||||
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
|
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
|
||||||
img.save(bio)
|
img.save(bio)
|
||||||
return bio.getvalue(), 200, {
|
return (
|
||||||
'Content-Type': 'image/svg+xml',
|
bio.getvalue(),
|
||||||
'Cache-Control': 'public,max-age=3600',
|
200,
|
||||||
}
|
{
|
||||||
|
"Content-Type": "image/svg+xml",
|
||||||
|
"Cache-Control": "public,max-age=3600",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route('/api/1/history.json')
|
|
||||||
|
@bp.route("/api/1/history.json")
|
||||||
def history():
|
def history():
|
||||||
return jsonify(gen_main_graph())
|
return jsonify(gen_main_graph())
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
import cygpio
|
import cygpio
|
||||||
|
|
||||||
cygpio.test()
|
cygpio.test()
|
||||||
|
|
|
@ -3,5 +3,5 @@ from distutils.extension import Extension
|
||||||
from Cython.Build import cythonize
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
ext_modules = cythonize([Extension("cygpio", ["cygpio.pyx"], libraries=["pigpio"])])
|
ext_modules=cythonize([Extension("cygpio", ["cygpio.pyx"], libraries=["pigpio"])])
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Backend(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
return b''
|
return b""
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
pass
|
pass
|
||||||
|
@ -32,7 +32,7 @@ class Backend(object):
|
||||||
class DummyBackend(Backend):
|
class DummyBackend(Backend):
|
||||||
def read(self):
|
def read(self):
|
||||||
time.sleep(0.005)
|
time.sleep(0.005)
|
||||||
return b''
|
return b""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -55,7 +55,7 @@ class RaspiBackend(Backend):
|
||||||
|
|
||||||
status = self.pi.bb_serial_read_open(self.rx_pin, 9600, 9)
|
status = self.pi.bb_serial_read_open(self.rx_pin, 9600, 9)
|
||||||
if status:
|
if status:
|
||||||
raise Exception('Port open failed: %d', status)
|
raise Exception("Port open failed: %d", status)
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
while True:
|
while True:
|
||||||
|
@ -70,9 +70,9 @@ class RaspiBackend(Backend):
|
||||||
wid = self.pi.wave_create()
|
wid = self.pi.wave_create()
|
||||||
|
|
||||||
self.pi.set_mode(self.tx_pin, pigpio.OUTPUT)
|
self.pi.set_mode(self.tx_pin, pigpio.OUTPUT)
|
||||||
self.pi.wave_send_once(wid) # transmit serial data
|
self.pi.wave_send_once(wid) # transmit serial data
|
||||||
|
|
||||||
while self.pi.wave_tx_busy(): # wait until all data sent
|
while self.pi.wave_tx_busy(): # wait until all data sent
|
||||||
time.sleep(0.001)
|
time.sleep(0.001)
|
||||||
|
|
||||||
self.pi.wave_delete(wid)
|
self.pi.wave_delete(wid)
|
||||||
|
@ -83,11 +83,13 @@ class RaspiBackend(Backend):
|
||||||
# Backend device based on STM32F1 MDB-USB adapter (to be actually designed...)
|
# Backend device based on STM32F1 MDB-USB adapter (to be actually designed...)
|
||||||
#
|
#
|
||||||
class SerialBackend(Backend):
|
class SerialBackend(Backend):
|
||||||
def __init__(self, device='/dev/serial/by-id/usb-vuko@hackerspace.pl_flowMeter_00001-if00'):
|
def __init__(
|
||||||
|
self, device="/dev/serial/by-id/usb-vuko@hackerspace.pl_flowMeter_00001-if00"
|
||||||
|
):
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
buf = b''
|
buf = b""
|
||||||
while len(buf) < 2:
|
while len(buf) < 2:
|
||||||
buf += self.ser.read(1)
|
buf += self.ser.read(1)
|
||||||
|
|
||||||
|
@ -103,6 +105,7 @@ class SerialBackend(Backend):
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
self.ser = serial.Serial(self.device)
|
self.ser = serial.Serial(self.device)
|
||||||
|
|
||||||
# FIXME clear buffer
|
# FIXME clear buffer
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Page 26 - peripheral addresses
|
# Page 26 - peripheral addresses
|
||||||
COIN_TYPE = 0x0c
|
COIN_TYPE = 0x0C
|
||||||
COIN_POLL = 0x0b
|
COIN_POLL = 0x0B
|
||||||
|
|
||||||
COIN_EXP = 0x0f
|
COIN_EXP = 0x0F
|
||||||
COIN_EXP_PAYOUT = 0x02
|
COIN_EXP_PAYOUT = 0x02
|
||||||
|
|
||||||
CASHLESS_RESET = 0x10
|
CASHLESS_RESET = 0x10
|
||||||
|
|
194
mdb/device.py
194
mdb/device.py
|
@ -16,6 +16,7 @@ from mdb.utils import compute_checksum, compute_chk, bcd_decode
|
||||||
from mdb.constants import *
|
from mdb.constants import *
|
||||||
from mdb.backend import RaspiBackend, DummyBackend, SerialBackend, pigpio
|
from mdb.backend import RaspiBackend, DummyBackend, SerialBackend, pigpio
|
||||||
|
|
||||||
|
|
||||||
class MDBRequest(object):
|
class MDBRequest(object):
|
||||||
timestamp = None
|
timestamp = None
|
||||||
data = None
|
data = None
|
||||||
|
@ -38,7 +39,7 @@ class MDBRequest(object):
|
||||||
try:
|
try:
|
||||||
if self.ack:
|
if self.ack:
|
||||||
if len(self.data) == 1 and self.processed:
|
if len(self.data) == 1 and self.processed:
|
||||||
return True # Only ACK
|
return True # Only ACK
|
||||||
|
|
||||||
return self.data[-2] == compute_checksum(self.command, self.data[:-2])
|
return self.data[-2] == compute_checksum(self.command, self.data[:-2])
|
||||||
else:
|
else:
|
||||||
|
@ -46,18 +47,21 @@ class MDBRequest(object):
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
logging.exception('Checksum validation failed for: %r %r', self.command, self.data)
|
logging.exception(
|
||||||
|
"Checksum validation failed for: %r %r", self.command, self.data
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ack(self):
|
def ack(self):
|
||||||
return len(self.data) and self.data[-1] == 0x00
|
return len(self.data) and self.data[-1] == 0x00
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<MDBRequest 0x%02x [%s] chk:%r>' % (
|
return "<MDBRequest 0x%02x [%s] chk:%r>" % (
|
||||||
self.command,
|
self.command,
|
||||||
' '.join(['0x%02x' % b for b in self.data]),
|
" ".join(["0x%02x" % b for b in self.data]),
|
||||||
self.validate_checksum()
|
self.validate_checksum(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MDBDevice(object):
|
class MDBDevice(object):
|
||||||
|
@ -71,15 +75,15 @@ class MDBDevice(object):
|
||||||
self.poll_queue = queue.Queue()
|
self.poll_queue = queue.Queue()
|
||||||
if cygpio:
|
if cygpio:
|
||||||
self.backend = cygpio.CythonRaspiBackend()
|
self.backend = cygpio.CythonRaspiBackend()
|
||||||
self.logger.warning('Running with FAST CYTHON BACKEND')
|
self.logger.warning("Running with FAST CYTHON BACKEND")
|
||||||
elif pigpio:
|
elif pigpio:
|
||||||
self.backend = RaspiBackend()
|
self.backend = RaspiBackend()
|
||||||
else:
|
else:
|
||||||
self.logger.warning('Running with dummy backend device')
|
self.logger.warning("Running with dummy backend device")
|
||||||
self.backend = SerialBackend()
|
self.backend = SerialBackend()
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
self.logger.info('Initializing... %r backend', self.backend)
|
self.logger.info("Initializing... %r backend", self.backend)
|
||||||
self.backend.open()
|
self.backend.open()
|
||||||
# here should IO / connection initizliation go
|
# here should IO / connection initizliation go
|
||||||
|
|
||||||
|
@ -92,24 +96,34 @@ class MDBDevice(object):
|
||||||
while True:
|
while True:
|
||||||
data = self.backend.read()
|
data = self.backend.read()
|
||||||
for b in range(0, len(data), 2):
|
for b in range(0, len(data), 2):
|
||||||
if data[b+1]:
|
if data[b + 1]:
|
||||||
if self.current_request: # and not self.current_request.processed:
|
if self.current_request: # and not self.current_request.processed:
|
||||||
if self.current_request.command not in [0xf2]:
|
if self.current_request.command not in [0xF2]:
|
||||||
self.logger.debug(self.current_request)
|
self.logger.debug(self.current_request)
|
||||||
if self.current_request.processed and self.current_request.ack:
|
if self.current_request.processed and self.current_request.ack:
|
||||||
self.logger.info('Got response: %d',self.current_request.data[-1])
|
self.logger.info(
|
||||||
|
"Got response: %d", self.current_request.data[-1]
|
||||||
|
)
|
||||||
self.poll_msg = []
|
self.poll_msg = []
|
||||||
|
|
||||||
self.current_request.reset(data[b])
|
self.current_request.reset(data[b])
|
||||||
self.send_buffer = None
|
self.send_buffer = None
|
||||||
elif self.current_request:
|
elif self.current_request:
|
||||||
if self.current_request.processed and data[b] == 0xaa and self.send_buffer:
|
if (
|
||||||
self.logger.warning('Received RETRANSMIT %r %r', self.current_request, self.send_buffer)
|
self.current_request.processed
|
||||||
#self.send(self.send_buffer, checksum=False)
|
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:
|
else:
|
||||||
self.current_request.data.append(data[b])
|
self.current_request.data.append(data[b])
|
||||||
else:
|
else:
|
||||||
self.logger.warning('Received unexpected data: 0x%02x', data[b])
|
self.logger.warning("Received unexpected data: 0x%02x", data[b])
|
||||||
|
|
||||||
if self.current_request and not self.current_request.processed:
|
if self.current_request and not self.current_request.processed:
|
||||||
try:
|
try:
|
||||||
|
@ -121,18 +135,20 @@ class MDBDevice(object):
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
self.logger.exception('Request process failed!')
|
self.logger.exception("Request process failed!")
|
||||||
|
|
||||||
def send(self, data, checksum=True):
|
def send(self, data, checksum=True):
|
||||||
data = list(data)
|
data = list(data)
|
||||||
if checksum:
|
if checksum:
|
||||||
data.append(0x100 | compute_chk(data))
|
data.append(0x100 | compute_chk(data))
|
||||||
|
|
||||||
msg = struct.pack('<%dh' % len(data), *data)
|
msg = struct.pack("<%dh" % len(data), *data)
|
||||||
|
|
||||||
self.send_buffer = data
|
self.send_buffer = data
|
||||||
|
|
||||||
self.logger.debug('>> [%d bytes] %s', len(data), ' '.join(['0x%02x' % b for b in data]))
|
self.logger.debug(
|
||||||
|
">> [%d bytes] %s", len(data), " ".join(["0x%02x" % b for b in data])
|
||||||
|
)
|
||||||
|
|
||||||
self.backend.write(msg)
|
self.backend.write(msg)
|
||||||
|
|
||||||
|
@ -140,46 +156,49 @@ class MDBDevice(object):
|
||||||
# Unimplemented...
|
# Unimplemented...
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# This is mostly working cashless device implementation
|
# This is mostly working cashless device implementation
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class CashlessMDBDevice(MDBDevice):
|
class CashlessMDBDevice(MDBDevice):
|
||||||
base_address = CASHLESS_RESET
|
base_address = CASHLESS_RESET
|
||||||
state = 'IDLE'
|
state = "IDLE"
|
||||||
last_poll = 0
|
last_poll = 0
|
||||||
config_data = [
|
config_data = [
|
||||||
0x01, # Feature level
|
0x01, # Feature level
|
||||||
0x19, 0x85, # PLN x---DD
|
0x19,
|
||||||
1, # scaling factor
|
0x85, # PLN x---DD
|
||||||
0x02, # decimal places factor
|
1, # scaling factor
|
||||||
10, # 10s response time
|
0x02, # decimal places factor
|
||||||
0x00, # misc options...
|
10, # 10s response time
|
||||||
]
|
0x00, # misc options...
|
||||||
|
]
|
||||||
|
|
||||||
manufacturer = 'GMD'
|
manufacturer = "GMD"
|
||||||
serial_number = '123456789012'
|
serial_number = "123456789012"
|
||||||
model_number = '123456789012'
|
model_number = "123456789012"
|
||||||
software_version = (0x21, 0x37)
|
software_version = (0x21, 0x37)
|
||||||
|
|
||||||
lockup_counter = 0
|
lockup_counter = 0
|
||||||
|
|
||||||
def process_request(self, req):
|
def process_request(self, req):
|
||||||
# FIXME this shouldn't be required...
|
# FIXME this shouldn't be required...
|
||||||
#if req.command == 0x30 and req.validate_checksum():
|
# if req.command == 0x30 and req.validate_checksum():
|
||||||
# self.lockup_counter += 1
|
# self.lockup_counter += 1
|
||||||
|
|
||||||
# if self.lockup_counter % 50 == 0:
|
# if self.lockup_counter % 50 == 0:
|
||||||
# self.logger.info('YOLO')
|
# self.logger.info('YOLO')
|
||||||
# return []
|
# return []
|
||||||
# return
|
# return
|
||||||
#if req.command == 0x31 and req.validate_checksum():
|
# if req.command == 0x31 and req.validate_checksum():
|
||||||
# return []
|
# return []
|
||||||
#if req.command == 0x37 and req.validate_checksum():
|
# if req.command == 0x37 and req.validate_checksum():
|
||||||
# return []
|
# return []
|
||||||
#if req.command == 0x36 and req.validate_checksum():
|
# if req.command == 0x36 and req.validate_checksum():
|
||||||
# return []
|
# return []
|
||||||
#if req.command == 0x34 and req.validate_checksum():
|
# if req.command == 0x34 and req.validate_checksum():
|
||||||
# return []
|
# return []
|
||||||
|
|
||||||
if (req.command & self.base_address) != self.base_address:
|
if (req.command & self.base_address) != self.base_address:
|
||||||
|
@ -193,8 +212,8 @@ class CashlessMDBDevice(MDBDevice):
|
||||||
self.lockup_counter = 0
|
self.lockup_counter = 0
|
||||||
|
|
||||||
if req.command == CASHLESS_RESET:
|
if req.command == CASHLESS_RESET:
|
||||||
self.state = 'RESET'
|
self.state = "RESET"
|
||||||
self.logger.info('RESET: Device reset')
|
self.logger.info("RESET: Device reset")
|
||||||
self.poll_queue.put([0x00])
|
self.poll_queue.put([0x00])
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
@ -209,89 +228,95 @@ class CashlessMDBDevice(MDBDevice):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.poll_msg:
|
if self.poll_msg:
|
||||||
self.logger.info('Sending POLL response: %r', self.poll_msg)
|
self.logger.info("Sending POLL response: %r", self.poll_msg)
|
||||||
|
|
||||||
return self.poll_msg
|
return self.poll_msg
|
||||||
|
|
||||||
elif req.command == CASHLESS_VEND:
|
elif req.command == CASHLESS_VEND:
|
||||||
if req.data[0] == 0x00: # vend request
|
if req.data[0] == 0x00: # vend request
|
||||||
self.logger.info('VEND: request %r', req)
|
self.logger.info("VEND: request %r", req)
|
||||||
value, product_bcd = struct.unpack('>xhhx', req.data)
|
value, product_bcd = struct.unpack(">xhhx", req.data)
|
||||||
product = bcd_decode(product_bcd)
|
product = bcd_decode(product_bcd)
|
||||||
self.logger.info('VEND: requested %d (%04x) for %d', product, product_bcd, value)
|
self.logger.info(
|
||||||
|
"VEND: requested %d (%04x) for %d", product, product_bcd, value
|
||||||
|
)
|
||||||
if self.vend_request(product, value):
|
if self.vend_request(product, value):
|
||||||
# accept. two latter bytes are value subtracted from balance
|
# accept. two latter bytes are value subtracted from balance
|
||||||
# displayed after purchase FIXME
|
# displayed after purchase FIXME
|
||||||
return [0x05, 0x00, 0xff]
|
return [0x05, 0x00, 0xFF]
|
||||||
else:
|
else:
|
||||||
# welp?
|
# welp?
|
||||||
return [0x06]
|
return [0x06]
|
||||||
|
|
||||||
elif req.data[0] == 0x01: # vend cancel
|
elif req.data[0] == 0x01: # vend cancel
|
||||||
self.logger.info('VEND: cancel')
|
self.logger.info("VEND: cancel")
|
||||||
return [0x06] # deny == ok
|
return [0x06] # deny == ok
|
||||||
|
|
||||||
elif req.data[0] == 0x02: # vend succ
|
elif req.data[0] == 0x02: # vend succ
|
||||||
self.logger.info('VEND: success %r', req)
|
self.logger.info("VEND: success %r", req)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
elif req.data[0] == 0x03:
|
elif req.data[0] == 0x03:
|
||||||
self.logger.info('VEND: failure')
|
self.logger.info("VEND: failure")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
elif req.data[0] == 0x04:
|
elif req.data[0] == 0x04:
|
||||||
self.logger.info('VEND: session complete %r', req)
|
self.logger.info("VEND: session complete %r", req)
|
||||||
self.state = 'OK'
|
self.state = "OK"
|
||||||
return [0x07]
|
return [0x07]
|
||||||
|
|
||||||
elif req.data[0] == 0x05:
|
elif req.data[0] == 0x05:
|
||||||
self.logger.info('VEND: cash sale')
|
self.logger.info("VEND: cash sale")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
elif req.data[0] == 0x06:
|
elif req.data[0] == 0x06:
|
||||||
self.logger.info('VEND: negative vend request')
|
self.logger.info("VEND: negative vend request")
|
||||||
return [0x06] # deny
|
return [0x06] # deny
|
||||||
else:
|
else:
|
||||||
self.logger.warning('VEND: unknown command %r', req)
|
self.logger.warning("VEND: unknown command %r", req)
|
||||||
|
|
||||||
elif req.command == CASHLESS_EXP and req.data[0] == CASHLESS_EXP_ID:
|
elif req.command == CASHLESS_EXP and req.data[0] == CASHLESS_EXP_ID:
|
||||||
self.logger.info('EXP_ID request: %r', req)
|
self.logger.info("EXP_ID request: %r", req)
|
||||||
return (
|
return (
|
||||||
bytearray([0x09]) + # peripheral ID
|
bytearray([0x09])
|
||||||
bytearray(self.manufacturer.rjust(3, ' ').encode('ascii')) +
|
+ bytearray( # peripheral ID
|
||||||
bytearray(self.serial_number.rjust(12, '0').encode('ascii')) +
|
self.manufacturer.rjust(3, " ").encode("ascii")
|
||||||
bytearray(self.model_number.rjust(12, '0').encode('ascii')) +
|
|
||||||
bytearray(self.software_version)
|
|
||||||
)
|
)
|
||||||
|
+ 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:
|
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]
|
vmc_level, disp_cols, disp_rows, disp_info = req.data[1:-1]
|
||||||
|
|
||||||
self.logger.info('SETUP config')
|
self.logger.info("SETUP config")
|
||||||
self.logger.info(' -> VMC level: %d', vmc_level)
|
self.logger.info(" -> VMC level: %d", vmc_level)
|
||||||
self.logger.info(' -> Disp cols: %d', disp_cols)
|
self.logger.info(" -> Disp cols: %d", disp_cols)
|
||||||
self.logger.info(' -> Disp rows: %d', disp_rows)
|
self.logger.info(" -> Disp rows: %d", disp_rows)
|
||||||
self.logger.info(' -> Disp info: %d', disp_info)
|
self.logger.info(" -> Disp info: %d", disp_info)
|
||||||
|
|
||||||
self.state = 'OK'
|
self.state = "OK"
|
||||||
|
|
||||||
return [0x01] + self.config_data
|
return [0x01] + self.config_data
|
||||||
|
|
||||||
elif req.command == CASHLESS_SETUP and req.data[0] == 0x01:
|
elif req.command == CASHLESS_SETUP and req.data[0] == 0x01:
|
||||||
self.logger.info('SETUP max/min price: %r', req)
|
self.logger.info("SETUP max/min price: %r", req)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
elif req.command == CASHLESS_READER:
|
elif req.command == CASHLESS_READER:
|
||||||
self.logger.info('READER update: %r', req.data[0])
|
self.logger.info("READER update: %r", req.data[0])
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def begin_session(self, amount):
|
def begin_session(self, amount):
|
||||||
self.logger.info('Beginning session for %d', amount)
|
self.logger.info("Beginning session for %d", amount)
|
||||||
# Begins new session with balance provided
|
# Begins new session with balance provided
|
||||||
if amount > 10000:
|
if amount > 10000:
|
||||||
amount = 10000
|
amount = 10000
|
||||||
|
|
||||||
self.poll_queue.put([0x03, (amount >> 8) & 0xff, amount & 0xff])
|
self.poll_queue.put([0x03, (amount >> 8) & 0xFF, amount & 0xFF])
|
||||||
|
|
||||||
def cancel_session(self):
|
def cancel_session(self):
|
||||||
# Cancels current session
|
# Cancels current session
|
||||||
|
@ -310,19 +335,34 @@ class CashlessMDBDevice(MDBDevice):
|
||||||
# This is mostly unfinished Bill validator implementation
|
# This is mostly unfinished Bill validator implementation
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class BillMDBDevice(MDBDevice):
|
class BillMDBDevice(MDBDevice):
|
||||||
scaling_factor = 50
|
scaling_factor = 50
|
||||||
|
|
||||||
bills = [
|
bills = [
|
||||||
50, 100, 200, 500, 1000, 2000, 5000, 10000,
|
50,
|
||||||
0, 0, 0, 0, 0, 0, 0, 0,
|
100,
|
||||||
]
|
200,
|
||||||
|
500,
|
||||||
|
1000,
|
||||||
|
2000,
|
||||||
|
5000,
|
||||||
|
10000,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
|
||||||
feed_bills = []
|
feed_bills = []
|
||||||
|
|
||||||
def feed_amount(self, amount):
|
def feed_amount(self, amount):
|
||||||
if amount % self.scaling_factor:
|
if amount % self.scaling_factor:
|
||||||
raise Exception('Invalid amount')
|
raise Exception("Invalid amount")
|
||||||
|
|
||||||
while amount > 0:
|
while amount > 0:
|
||||||
bills_list = filter(lambda v: v <= amount, self.bills)
|
bills_list = filter(lambda v: v <= amount, self.bills)
|
||||||
|
|
16
mdb/utils.py
16
mdb/utils.py
|
@ -1,14 +1,18 @@
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
def compute_chk(data):
|
def compute_chk(data):
|
||||||
return reduce(lambda a, b: (a+b)%256, data, 0)
|
return reduce(lambda a, b: (a + b) % 256, data, 0)
|
||||||
|
|
||||||
|
|
||||||
def compute_checksum(cmd, data):
|
def compute_checksum(cmd, data):
|
||||||
return compute_chk(bytearray([cmd]) + data)
|
return compute_chk(bytearray([cmd]) + data)
|
||||||
|
|
||||||
|
|
||||||
def bcd_decode(b):
|
def bcd_decode(b):
|
||||||
return \
|
return (
|
||||||
1000 * ((b & 0xf000) >> 12) + \
|
1000 * ((b & 0xF000) >> 12)
|
||||||
100 * ((b & 0xf00) >> 8) + \
|
+ 100 * ((b & 0xF00) >> 8)
|
||||||
10 * ((b & 0xf0) >> 4) + \
|
+ 10 * ((b & 0xF0) >> 4)
|
||||||
(b & 0x0f)
|
+ (b & 0x0F)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue