*: reformat using black
parent
c888873b7a
commit
d5896d5d16
56
accept.py
56
accept.py
|
@ -5,69 +5,69 @@ import threading
|
|||
import pprint
|
||||
import requests
|
||||
|
||||
INPUT_ADDR = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7' # bitvend addr
|
||||
INPUT_ADDR = "12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7" # bitvend 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):
|
||||
# Returns satoshi in local lowest denomination currency (grosze)
|
||||
rate = get_exchange_rate()
|
||||
return int(sat / 1000000.0 * rate)
|
||||
|
||||
|
||||
def process_transaction(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'] == INPUT_ADDR
|
||||
], 0)
|
||||
fee = sum([i['prev_out']['value'] for i in tx['x']['inputs']]) - \
|
||||
sum([o['value'] for o in tx['x']['out']])
|
||||
tx_size = tx["x"]["size"]
|
||||
tx_hash = tx["x"]["hash"]
|
||||
tx_value = sum([o["value"] for o in tx["x"]["out"] if o["addr"] == INPUT_ADDR], 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
|
||||
|
||||
print(tx_size, tx_hash, tx_value, fee, fee_byte)
|
||||
print(to_local_currency(tx_value))
|
||||
|
||||
|
||||
def on_message(ws, message):
|
||||
# print message
|
||||
data = json.loads(message)
|
||||
if data['op'] == 'utx':
|
||||
if data["op"] == "utx":
|
||||
process_transaction(data)
|
||||
pprint.pprint(data)
|
||||
for d in data['x']['out']:
|
||||
for d in data["x"]["out"]:
|
||||
pprint.pprint(d)
|
||||
|
||||
|
||||
def on_error(ws, error):
|
||||
print(error)
|
||||
|
||||
|
||||
def on_close(ws):
|
||||
print("### closed ###")
|
||||
|
||||
|
||||
def on_open(ws):
|
||||
print("### connected ###")
|
||||
ws.send(json.dumps({
|
||||
"op": "addr_sub",
|
||||
"addr": INPUT_ADDR
|
||||
}))
|
||||
ws.send(json.dumps({
|
||||
"op": "addr_sub",
|
||||
"addr": "1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22"
|
||||
}))
|
||||
ws.send(json.dumps({"op": "addr_sub", "addr": INPUT_ADDR}))
|
||||
ws.send(
|
||||
json.dumps({"op": "addr_sub", "addr": "1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22"})
|
||||
)
|
||||
|
||||
def run(*args):
|
||||
while True:
|
||||
time.sleep(20)
|
||||
ws.send(json.dumps({
|
||||
"op": "ping"
|
||||
}))
|
||||
ws.send(json.dumps({"op": "ping"}))
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
'''
|
||||
"""
|
||||
process_transaction({'op': 'utx',
|
||||
'x': {'hash': '03ed0015c29dfc3bfd3d8a215490d82e3562fe4e8bf5f2ffa737ac8fdc850cc4',
|
||||
'inputs': [{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
|
||||
|
@ -120,12 +120,14 @@ if __name__ == "__main__":
|
|||
'vin_sz': 3,
|
||||
'vout_sz': 2}})
|
||||
exit(0)
|
||||
'''
|
||||
"""
|
||||
websocket.enableTrace(True)
|
||||
ws = websocket.WebSocketApp("wss://ws.blockchain.info/inv",
|
||||
ws = websocket.WebSocketApp(
|
||||
"wss://ws.blockchain.info/inv",
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close = on_close)
|
||||
on_close=on_close,
|
||||
)
|
||||
ws.on_open = on_open
|
||||
print("### running... ###")
|
||||
ws.run_forever()
|
||||
|
|
|
@ -9,6 +9,7 @@ from bitvend import create_app, dev, proc, db
|
|||
|
||||
if __name__ == "__main__":
|
||||
from prometheus_client import start_http_server
|
||||
|
||||
start_http_server(8000)
|
||||
|
||||
app = create_app()
|
||||
|
@ -16,8 +17,12 @@ if __name__ == "__main__":
|
|||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
threading.Thread(target=app.run, kwargs={
|
||||
'host': '0.0.0.0',
|
||||
}, daemon=True).start()
|
||||
threading.Thread(
|
||||
target=app.run,
|
||||
kwargs={
|
||||
"host": "0.0.0.0",
|
||||
},
|
||||
daemon=True,
|
||||
).start()
|
||||
# proc.start()
|
||||
dev.run()
|
||||
|
|
|
@ -9,13 +9,13 @@ dev = BitvendCashlessMDBDevice()
|
|||
proc = PaymentProcessor(dev)
|
||||
spaceauth = SpaceAuth()
|
||||
|
||||
from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
|
||||
sat_to_btc
|
||||
from bitvend.utils import to_local_currency, from_local_currency, format_btc, sat_to_btc
|
||||
from bitvend.models import db, Transaction, User
|
||||
|
||||
import bitvend.views
|
||||
import bitvend.admin
|
||||
|
||||
|
||||
@spaceauth.user_loader
|
||||
def bitvend_user_loader(username, profile=None):
|
||||
u = User.find(username)
|
||||
|
@ -27,15 +27,21 @@ def bitvend_user_loader(username, profile=None):
|
|||
|
||||
return u
|
||||
|
||||
|
||||
def create_app():
|
||||
app = flask.Flask(__name__)
|
||||
app.config.from_object('bitvend.default_settings')
|
||||
print('Loading extra settings from {}...'.format(os.environ.get('BITVEND_SETTINGS', '')))
|
||||
app.config.from_pyfile(os.environ.get('BITVEND_SETTINGS', ''), silent=True)
|
||||
app.config.from_object("bitvend.default_settings")
|
||||
print(
|
||||
"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
|
||||
# url_for(_external=True)
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
db.init_app(app)
|
||||
|
@ -44,27 +50,31 @@ def create_app():
|
|||
proc.init_app(app)
|
||||
|
||||
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
|
||||
def ctx_utils():
|
||||
return {
|
||||
'from_local_currency': from_local_currency,
|
||||
'to_local_currency': to_local_currency,
|
||||
'format_btc': format_btc,
|
||||
'sat_to_btc': sat_to_btc,
|
||||
'qrcode': lambda data: flask.url_for('bitvend.qrcode_gen', data=data),
|
||||
'current_transaction': Transaction.query.filter(Transaction.finished == False).first(),
|
||||
'mdb_online': dev.online,
|
||||
'proc_online': proc.online,
|
||||
'static': lambda fn, **kwargs: flask.url_for('static', filename=fn,
|
||||
**kwargs)
|
||||
"from_local_currency": from_local_currency,
|
||||
"to_local_currency": to_local_currency,
|
||||
"format_btc": format_btc,
|
||||
"sat_to_btc": sat_to_btc,
|
||||
"qrcode": lambda data: flask.url_for("bitvend.qrcode_gen", data=data),
|
||||
"current_transaction": Transaction.query.filter(
|
||||
Transaction.finished == False
|
||||
).first(),
|
||||
"mdb_online": dev.online,
|
||||
"proc_online": proc.online,
|
||||
"static": lambda fn, **kwargs: flask.url_for(
|
||||
"static", filename=fn, **kwargs
|
||||
),
|
||||
}
|
||||
|
||||
def url_for_other_page(page):
|
||||
args = flask.request.view_args.copy()
|
||||
args['page'] = page
|
||||
args["page"] = page
|
||||
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
|
||||
|
|
|
@ -7,46 +7,46 @@ from bitvend.forms import ManualForm
|
|||
from spaceauth import cap_required
|
||||
|
||||
|
||||
admin_required = cap_required('staff')
|
||||
bp = Blueprint('admin', __name__)
|
||||
admin_required = cap_required("staff")
|
||||
bp = Blueprint("admin", __name__)
|
||||
|
||||
@bp.route('/manual', methods=['GET', 'POST'])
|
||||
|
||||
@bp.route("/manual", methods=["GET", "POST"])
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def manual():
|
||||
form = ManualForm()
|
||||
if form.validate_on_submit():
|
||||
current_user.transactions.append(Transaction(
|
||||
amount=form.amount.data
|
||||
))
|
||||
current_user.transactions.append(Transaction(amount=form.amount.data))
|
||||
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
|
||||
@admin_required
|
||||
def transactions(page):
|
||||
return render_template('admin/transactions.html',
|
||||
transactions=Transaction.query.paginate(page)
|
||||
return render_template(
|
||||
"admin/transactions.html", transactions=Transaction.query.paginate(page)
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/begin')
|
||||
@bp.route("/begin")
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def begin():
|
||||
dev.begin_session(500)
|
||||
flash('Operation successful.', 'success')
|
||||
return redirect('/')
|
||||
flash("Operation successful.", "success")
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@bp.route('/cancel')
|
||||
@bp.route("/cancel")
|
||||
@fresh_login_required
|
||||
@admin_required
|
||||
def cancel():
|
||||
dev.cancel_session()
|
||||
flash('Operation successful.', 'success')
|
||||
return redirect('/')
|
||||
flash("Operation successful.", "success")
|
||||
return redirect("/")
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
import platform
|
||||
|
||||
SECRET_KEY = 'testing'
|
||||
SECRET_KEY = "testing"
|
||||
|
||||
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'
|
||||
BLOCKCYPHER_CHAIN = 'btc/main'
|
||||
INPUT_ADDRESS = "12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7"
|
||||
BLOCKCYPHER_CHAIN = "btc/main"
|
||||
|
||||
# INPUT_ADDRESS ='n2SYFMbgfG4LXkvB4VgF4SeSEqQE28NAuV'
|
||||
# BLOCKCYPHER_CHAIN = 'btc/test3'
|
||||
|
||||
BLOCKCYPHER_TOKEN = '918ddf2a06184ec295ca1cb636db20b5'
|
||||
BLOCKCYPHER_TOKEN = "918ddf2a06184ec295ca1cb636db20b5"
|
||||
|
||||
TEMPLATES_AUTO_RELOAD = True
|
||||
ITEMS = [
|
||||
{
|
||||
'name': 'Club Mate',
|
||||
'image': '/static/img/club-mate.png',
|
||||
'value': 500,
|
||||
"name": "Club Mate",
|
||||
"image": "/static/img/club-mate.png",
|
||||
"value": 500,
|
||||
},
|
||||
{
|
||||
'name': 'Mate Mate',
|
||||
'image': '/static/img/mate-mate.png',
|
||||
'value': 600,
|
||||
"name": "Mate Mate",
|
||||
"image": "/static/img/mate-mate.png",
|
||||
"value": 600,
|
||||
},
|
||||
{
|
||||
'name': 'Arduino Pro Micro',
|
||||
'image': '/static/img/promicro.png',
|
||||
'value': 1600,
|
||||
"name": "Arduino Pro Micro",
|
||||
"image": "/static/img/promicro.png",
|
||||
"value": 1600,
|
||||
},
|
||||
{
|
||||
'name': 'Arduino Pro Mini',
|
||||
'image': '/static/img/promini.png',
|
||||
'value': 750,
|
||||
"name": "Arduino Pro Mini",
|
||||
"image": "/static/img/promini.png",
|
||||
"value": 750,
|
||||
},
|
||||
{
|
||||
'name': 'NodeMCU (ESP8266)',
|
||||
'image': '/static/img/nodemcu.png',
|
||||
'value': 1700,
|
||||
"name": "NodeMCU (ESP8266)",
|
||||
"image": "/static/img/nodemcu.png",
|
||||
"value": 1700,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ class DecimalUnityField(DecimalField):
|
|||
|
||||
def _value(self):
|
||||
if self.data is not None:
|
||||
format = '%%0.%df' % self.places
|
||||
return (format % (Decimal(self.data) / self.unity,))
|
||||
format = "%%0.%df" % self.places
|
||||
return format % (Decimal(self.data) / self.unity,)
|
||||
elif self.raw_data:
|
||||
return self.raw_data[0]
|
||||
|
||||
|
@ -28,21 +28,29 @@ class DecimalUnityField(DecimalField):
|
|||
except InvalidOperation:
|
||||
self.data = None
|
||||
|
||||
|
||||
def UserExists(form, field):
|
||||
if not User.query.get(field.data):
|
||||
raise ValidationError('User does not exist.')
|
||||
raise ValidationError("User does not exist.")
|
||||
|
||||
|
||||
def NotCurrentUser(form, field):
|
||||
if field.data == current_user.uid:
|
||||
raise ValidationError('Are you serious?')
|
||||
raise ValidationError("Are you serious?")
|
||||
|
||||
|
||||
class TransferForm(FlaskForm):
|
||||
target = StringField("Target user", validators=[
|
||||
DataRequired(), UserExists, NotCurrentUser])
|
||||
amount = DecimalUnityField("Amount", default=0, validators=[
|
||||
target = StringField(
|
||||
"Target user", validators=[DataRequired(), UserExists, NotCurrentUser]
|
||||
)
|
||||
amount = DecimalUnityField(
|
||||
"Amount",
|
||||
default=0,
|
||||
validators=[
|
||||
NumberRange(min=1),
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ManualForm(FlaskForm):
|
||||
amount = DecimalUnityField("Amount", default=0, validators=[
|
||||
])
|
||||
amount = DecimalUnityField("Amount", default=0, validators=[])
|
||||
|
|
|
@ -11,7 +11,9 @@ def daterange(start_date, end_date):
|
|||
|
||||
|
||||
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:
|
||||
date_from = min(date_hash.keys())
|
||||
|
@ -26,14 +28,12 @@ def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
|
|||
|
||||
|
||||
def gen_database_graph(title, query, date_from, date_to, default=0):
|
||||
date_column = query.column_descriptions[0]['expr']
|
||||
resultset = query \
|
||||
.group_by(
|
||||
date_column
|
||||
).filter(
|
||||
date_column >= date_from,
|
||||
date_column <= date_to
|
||||
).all()
|
||||
date_column = query.column_descriptions[0]["expr"]
|
||||
resultset = (
|
||||
query.group_by(date_column)
|
||||
.filter(date_column >= date_from, date_column <= date_to)
|
||||
.all()
|
||||
)
|
||||
|
||||
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_to = datetime.combine(date_to, datetime.max.time())
|
||||
|
||||
return zip_graph([
|
||||
['date'] + [n.strftime('%Y-%m-%d')
|
||||
for n in daterange(date_from, date_to)],
|
||||
|
||||
gen_database_graph('purchases', db.session.query(
|
||||
return zip_graph(
|
||||
[
|
||||
["date"] + [n.strftime("%Y-%m-%d") for n in daterange(date_from, date_to)],
|
||||
gen_database_graph(
|
||||
"purchases",
|
||||
db.session.query(
|
||||
# func.date_trunc('day', Transaction.created),
|
||||
func.strftime('%Y-%m-%d', Transaction.created),
|
||||
func.strftime("%Y-%m-%d", Transaction.created),
|
||||
func.count(Transaction.created),
|
||||
).filter(Transaction.type == 'purchase'), date_from, date_to),
|
||||
])
|
||||
).filter(Transaction.type == "purchase"),
|
||||
date_from,
|
||||
date_to,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -22,11 +22,11 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
|
|||
def vend_request(self, product, value):
|
||||
# FIXME we report success here, because database write takes too much
|
||||
# time to respond in 5ms.
|
||||
self.send([0x05, 0x00, 0xff])
|
||||
self.send([0x05, 0x00, 0xFF])
|
||||
self.poll_queue.put([0x05])
|
||||
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:
|
||||
with self.app.app_context():
|
||||
|
@ -48,20 +48,30 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
|
|||
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')
|
||||
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')
|
||||
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')
|
||||
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()
|
||||
|
||||
|
|
|
@ -7,21 +7,26 @@ from sqlalchemy.sql import func, select, and_
|
|||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class TransferException(Exception): pass
|
||||
class NoFunds(TransferException): pass
|
||||
|
||||
class TransferException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoFunds(TransferException):
|
||||
pass
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
__tablename__ = "users"
|
||||
|
||||
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):
|
||||
return self.uid
|
||||
|
||||
def __repr__(self):
|
||||
return '<User {0.uid} {0.balance}>'.format(self)
|
||||
return "<User {0.uid} {0.balance}>".format(self)
|
||||
|
||||
@hybrid_property
|
||||
def balance(self):
|
||||
|
@ -29,51 +34,53 @@ class User(db.Model):
|
|||
|
||||
@balance.expression
|
||||
def balance(self):
|
||||
return (select([func.sum(Transaction.amount)]).
|
||||
where(Transaction.uid == User.uid).
|
||||
label("balance")
|
||||
return (
|
||||
select([func.sum(Transaction.amount)])
|
||||
.where(Transaction.uid == User.uid)
|
||||
.label("balance")
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
def purchase_count(self):
|
||||
return self.transactions.filter(Transaction.type == 'purchase').count()
|
||||
return self.transactions.filter(Transaction.type == "purchase").count()
|
||||
|
||||
@purchase_count.expression
|
||||
def purchase_count(self):
|
||||
return (select([func.count(Transaction.amount)]).
|
||||
where(and_(
|
||||
Transaction.uid == User.uid,
|
||||
Transaction.type == 'purchase')).
|
||||
label("purchase_count")
|
||||
return (
|
||||
select([func.count(Transaction.amount)])
|
||||
.where(and_(Transaction.uid == User.uid, Transaction.type == "purchase"))
|
||||
.label("purchase_count")
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
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
|
||||
def purchase_amount(self):
|
||||
return (select([-func.sum(Transaction.amount)]).
|
||||
where(and_(
|
||||
Transaction.uid == User.uid,
|
||||
Transaction.type == 'purchase')).
|
||||
label("purchase_amount")
|
||||
return (
|
||||
select([-func.sum(Transaction.amount)])
|
||||
.where(and_(Transaction.uid == User.uid, Transaction.type == "purchase"))
|
||||
.label("purchase_amount")
|
||||
)
|
||||
|
||||
def transfer(self, target, amount):
|
||||
if amount > self.amount_available:
|
||||
raise NoFunds()
|
||||
|
||||
self.transactions.append(Transaction(
|
||||
amount=-amount, type='transfer', related=target.uid
|
||||
))
|
||||
target.transactions.append(Transaction(
|
||||
amount=amount, type='transfer', related=self.uid
|
||||
))
|
||||
self.transactions.append(
|
||||
Transaction(amount=-amount, type="transfer", related=target.uid)
|
||||
)
|
||||
target.transactions.append(
|
||||
Transaction(amount=amount, type="transfer", related=self.uid)
|
||||
)
|
||||
|
||||
@property
|
||||
def debt_limit(self):
|
||||
return app.config.get('DEBT_LIMIT', 5000)
|
||||
return app.config.get("DEBT_LIMIT", 5000)
|
||||
|
||||
@hybrid_property
|
||||
def amount_available(self):
|
||||
|
@ -96,18 +103,20 @@ class User(db.Model):
|
|||
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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)
|
||||
|
||||
type = db.Column(db.String(32), default='manual')
|
||||
type = db.Column(db.String(32), default="manual")
|
||||
|
||||
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)
|
||||
|
||||
|
@ -121,11 +130,9 @@ class Transaction(db.Model):
|
|||
|
||||
@hybrid_property
|
||||
def finished(self):
|
||||
return (self.type != 'purchase') | (self.product_id != None)
|
||||
return (self.type != "purchase") | (self.product_id != None)
|
||||
|
||||
__mapper_args__ = {
|
||||
"order_by": created.desc()
|
||||
}
|
||||
__mapper_args__ = {"order_by": created.desc()}
|
||||
|
||||
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
|
||||
app = None
|
||||
|
||||
def __init__(
|
||||
self, device, input_address=None, chain_id=None, app=None,
|
||||
token=None):
|
||||
def __init__(self, device, input_address=None, chain_id=None, app=None, token=None):
|
||||
super(PaymentProcessor, self).__init__()
|
||||
self.device = device
|
||||
self.input_address = input_address
|
||||
|
@ -32,45 +30,47 @@ class PaymentProcessor(threading.Thread):
|
|||
self.app = app
|
||||
|
||||
if not self.input_address:
|
||||
self.input_address = self.app.config['INPUT_ADDRESS']
|
||||
self.chain_id = self.app.config['BLOCKCYPHER_CHAIN']
|
||||
self.token = self.app.config['BLOCKCYPHER_TOKEN']
|
||||
self.input_address = self.app.config["INPUT_ADDRESS"]
|
||||
self.chain_id = self.app.config["BLOCKCYPHER_CHAIN"]
|
||||
self.token = self.app.config["BLOCKCYPHER_TOKEN"]
|
||||
|
||||
def run(self):
|
||||
self.logger.info('Starting...')
|
||||
self.logger.info("Starting...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
ws = websocket.WebSocketApp(
|
||||
"wss://socket.blockcypher.com/v1/%s?token=%s" \
|
||||
"wss://socket.blockcypher.com/v1/%s?token=%s"
|
||||
% (self.chain_id, self.token),
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_close=self.on_close,
|
||||
on_open=self.on_open)
|
||||
on_open=self.on_open,
|
||||
)
|
||||
|
||||
ws.run_forever(ping_timeout=20, ping_interval=30)
|
||||
except:
|
||||
self.logger.exception('run_forever failed')
|
||||
self.logger.exception("run_forever failed")
|
||||
time.sleep(1)
|
||||
|
||||
def process_transaction(self, tx):
|
||||
tx_size = tx['size']
|
||||
tx_hash = tx['hash']
|
||||
tx_value = sum([
|
||||
o['value'] for o in tx['outputs'] if self.input_address in o['addresses']
|
||||
], 0)
|
||||
fee = tx['fees']
|
||||
tx_size = tx["size"]
|
||||
tx_hash = tx["hash"]
|
||||
tx_value = sum(
|
||||
[o["value"] for o in tx["outputs"] if self.input_address in o["addresses"]],
|
||||
0,
|
||||
)
|
||||
fee = tx["fees"]
|
||||
|
||||
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))
|
||||
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():
|
||||
intx = Transaction(
|
||||
uid='__bitcoin__',
|
||||
type='bitcoin',
|
||||
uid="__bitcoin__",
|
||||
type="bitcoin",
|
||||
amount=to_local_currency(tx_value),
|
||||
tx_hash=tx_hash,
|
||||
)
|
||||
|
@ -78,8 +78,8 @@ class PaymentProcessor(threading.Thread):
|
|||
db.session.commit()
|
||||
|
||||
outtx = Transaction(
|
||||
uid='__bitcoin__',
|
||||
type='purchase',
|
||||
uid="__bitcoin__",
|
||||
type="purchase",
|
||||
)
|
||||
db.session.add(outtx)
|
||||
db.session.commit()
|
||||
|
@ -87,14 +87,14 @@ class PaymentProcessor(threading.Thread):
|
|||
tx_id = outtx.id
|
||||
|
||||
if to_local_currency(tx_value) < 100:
|
||||
self.logger.warning('Whyyyy so low...')
|
||||
self.logger.warning("Whyyyy so low...")
|
||||
return
|
||||
|
||||
if fee_byte < 15:
|
||||
self.logger.warning('Fee too low...')
|
||||
self.logger.warning("Fee too low...")
|
||||
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...
|
||||
self.device.begin_session(to_local_currency(tx_value), tx_id)
|
||||
|
@ -102,27 +102,31 @@ class PaymentProcessor(threading.Thread):
|
|||
def on_message(self, ws, message):
|
||||
# print 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)
|
||||
elif data.get('event') == 'pong':
|
||||
elif data.get("event") == "pong":
|
||||
self.last_pong = time.time()
|
||||
|
||||
def on_error(self, ws, error):
|
||||
self.logger.error(error)
|
||||
|
||||
def on_close(self, ws):
|
||||
self.logger.info('Connection closed')
|
||||
self.logger.info("Connection closed")
|
||||
|
||||
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(
|
||||
json.dumps(
|
||||
{
|
||||
"event": "tx-confidence",
|
||||
"address": self.input_address,
|
||||
"confidence": 0.9,
|
||||
}))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
while True:
|
||||
# FIXME check last ping time
|
||||
ws.send(json.dumps({
|
||||
"event": "ping"
|
||||
}))
|
||||
ws.send(json.dumps({"event": "ping"}))
|
||||
time.sleep(20)
|
||||
|
||||
if time.time() - self.last_pong > 60:
|
||||
self.logger.warning('Closing socket for inactivity')
|
||||
self.logger.warning("Closing socket for inactivity")
|
||||
ws.close()
|
||||
return
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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')
|
||||
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"
|
||||
)
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
import cachetools
|
||||
import requests
|
||||
|
||||
|
||||
@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
|
||||
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):
|
||||
# Returns satoshi in local lowest denomination currency (grosze)
|
||||
|
@ -18,6 +20,7 @@ def to_local_currency(sat, safe=False):
|
|||
raise
|
||||
return int(sat / 1000000.0 * rate)
|
||||
|
||||
|
||||
def from_local_currency(val, safe=False):
|
||||
# Returns satoshi value from local currency
|
||||
try:
|
||||
|
@ -29,10 +32,12 @@ def from_local_currency(val, safe=False):
|
|||
|
||||
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('.')
|
||||
return ("฿%.8f" % (sat_to_btc(amount),)).rstrip("0").rstrip(".")
|
||||
|
|
116
bitvend/views.py
116
bitvend/views.py
|
@ -1,5 +1,4 @@
|
|||
from flask import Blueprint, render_template, redirect, request, flash, \
|
||||
url_for, jsonify
|
||||
from flask import Blueprint, render_template, redirect, request, flash, url_for, jsonify
|
||||
from flask import current_app as app
|
||||
import six
|
||||
|
||||
|
@ -13,35 +12,40 @@ from bitvend.graphs import gen_main_graph
|
|||
|
||||
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():
|
||||
transactions = []
|
||||
hallofshame = User.query \
|
||||
.with_entities(User, User.balance) \
|
||||
.order_by(User.balance.asc()) \
|
||||
.filter(User.balance < 0) \
|
||||
.limit(5) \
|
||||
hallofshame = (
|
||||
User.query.with_entities(User, User.balance)
|
||||
.order_by(User.balance.asc())
|
||||
.filter(User.balance < 0)
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
hallofaddicts = User.query \
|
||||
.with_entities(User, User.purchase_amount, User.purchase_count) \
|
||||
.order_by(User.purchase_amount.desc()) \
|
||||
.filter(User.purchase_amount > 0) \
|
||||
.limit(5) \
|
||||
hallofaddicts = (
|
||||
User.query.with_entities(User, User.purchase_amount, User.purchase_count)
|
||||
.order_by(User.purchase_amount.desc())
|
||||
.filter(User.purchase_amount > 0)
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
bottles_purchased = Transaction.query \
|
||||
.filter(Transaction.amount.in_([-500, -600]), Transaction.type == 'purchase') \
|
||||
.count()
|
||||
bottles_purchased = Transaction.query.filter(
|
||||
Transaction.amount.in_([-500, -600]), Transaction.type == "purchase"
|
||||
).count()
|
||||
|
||||
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(
|
||||
'index.html',
|
||||
items=app.config['ITEMS'],
|
||||
"index.html",
|
||||
items=app.config["ITEMS"],
|
||||
transactions=transactions,
|
||||
transfer_form=TransferForm(),
|
||||
hallofshame=hallofshame,
|
||||
|
@ -49,56 +53,62 @@ def index():
|
|||
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):
|
||||
return render_template('transactions.html',
|
||||
transactions=current_user.transactions.paginate(page)
|
||||
return render_template(
|
||||
"transactions.html", transactions=current_user.transactions.paginate(page)
|
||||
)
|
||||
|
||||
@bp.route('/transfer', methods=['GET', 'POST'])
|
||||
|
||||
@bp.route("/transfer", methods=["GET", "POST"])
|
||||
def transfer():
|
||||
transfer_form = TransferForm()
|
||||
|
||||
if transfer_form.validate_on_submit():
|
||||
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()
|
||||
flash('Transfer succeeded.', 'success')
|
||||
flash("Transfer succeeded.", "success")
|
||||
except NoFunds:
|
||||
flash('No funds.', 'danger')
|
||||
return redirect(url_for('.index'))
|
||||
flash("No funds.", "danger")
|
||||
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
|
||||
@cap_required('staff')
|
||||
@cap_required("staff")
|
||||
def log():
|
||||
return render_template(
|
||||
'log.html', transactions=Transaction.query.all())
|
||||
return render_template("log.html", transactions=Transaction.query.all())
|
||||
|
||||
@bp.route('/begin')
|
||||
|
||||
@bp.route("/begin")
|
||||
@login_required
|
||||
def begin():
|
||||
if Transaction.query.filter(Transaction.finished == False).count():
|
||||
flash('Nope xD', 'danger')
|
||||
return redirect(url_for('.index'))
|
||||
flash("Nope xD", "danger")
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
if current_user.amount_available <= 0:
|
||||
flash('Nope xD', 'danger')
|
||||
return redirect(url_for('.index'))
|
||||
flash("Nope xD", "danger")
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
tx = Transaction(type='purchase')
|
||||
tx = Transaction(type="purchase")
|
||||
current_user.transactions.append(tx)
|
||||
db.session.commit()
|
||||
|
||||
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
|
||||
def cancel():
|
||||
dev.cancel_session()
|
||||
|
@ -108,20 +118,26 @@ def cancel():
|
|||
Transaction.query.filter(Transaction.finished == False).delete()
|
||||
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):
|
||||
bio = six.BytesIO()
|
||||
qr = qrcode.QRCode(border=0, box_size=50)
|
||||
qr.add_data(data)
|
||||
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
|
||||
img.save(bio)
|
||||
return bio.getvalue(), 200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'public,max-age=3600',
|
||||
}
|
||||
return (
|
||||
bio.getvalue(),
|
||||
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():
|
||||
return jsonify(gen_main_graph())
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
import cygpio
|
||||
|
||||
cygpio.test()
|
||||
|
|
|
@ -20,7 +20,7 @@ class Backend(object):
|
|||
pass
|
||||
|
||||
def read(self):
|
||||
return b''
|
||||
return b""
|
||||
|
||||
def write(self, data):
|
||||
pass
|
||||
|
@ -32,7 +32,7 @@ class Backend(object):
|
|||
class DummyBackend(Backend):
|
||||
def read(self):
|
||||
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)
|
||||
if status:
|
||||
raise Exception('Port open failed: %d', status)
|
||||
raise Exception("Port open failed: %d", status)
|
||||
|
||||
def read(self):
|
||||
while True:
|
||||
|
@ -83,11 +83,13 @@ class RaspiBackend(Backend):
|
|||
# Backend device based on STM32F1 MDB-USB adapter (to be actually designed...)
|
||||
#
|
||||
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
|
||||
|
||||
def read(self):
|
||||
buf = b''
|
||||
buf = b""
|
||||
while len(buf) < 2:
|
||||
buf += self.ser.read(1)
|
||||
|
||||
|
@ -103,6 +105,7 @@ class SerialBackend(Backend):
|
|||
|
||||
def open(self):
|
||||
import serial
|
||||
|
||||
self.ser = serial.Serial(self.device)
|
||||
|
||||
# FIXME clear buffer
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Page 26 - peripheral addresses
|
||||
COIN_TYPE = 0x0c
|
||||
COIN_POLL = 0x0b
|
||||
COIN_TYPE = 0x0C
|
||||
COIN_POLL = 0x0B
|
||||
|
||||
COIN_EXP = 0x0f
|
||||
COIN_EXP = 0x0F
|
||||
COIN_EXP_PAYOUT = 0x02
|
||||
|
||||
CASHLESS_RESET = 0x10
|
||||
|
|
150
mdb/device.py
150
mdb/device.py
|
@ -16,6 +16,7 @@ from mdb.utils import compute_checksum, compute_chk, bcd_decode
|
|||
from mdb.constants import *
|
||||
from mdb.backend import RaspiBackend, DummyBackend, SerialBackend, pigpio
|
||||
|
||||
|
||||
class MDBRequest(object):
|
||||
timestamp = None
|
||||
data = None
|
||||
|
@ -46,17 +47,20 @@ class MDBRequest(object):
|
|||
except KeyboardInterrupt:
|
||||
raise
|
||||
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
|
||||
|
||||
@property
|
||||
def ack(self):
|
||||
return len(self.data) and self.data[-1] == 0x00
|
||||
|
||||
def __repr__(self):
|
||||
return '<MDBRequest 0x%02x [%s] chk:%r>' % (
|
||||
return "<MDBRequest 0x%02x [%s] chk:%r>" % (
|
||||
self.command,
|
||||
' '.join(['0x%02x' % b for b in self.data]),
|
||||
self.validate_checksum()
|
||||
" ".join(["0x%02x" % b for b in self.data]),
|
||||
self.validate_checksum(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -71,15 +75,15 @@ class MDBDevice(object):
|
|||
self.poll_queue = queue.Queue()
|
||||
if cygpio:
|
||||
self.backend = cygpio.CythonRaspiBackend()
|
||||
self.logger.warning('Running with FAST CYTHON BACKEND')
|
||||
self.logger.warning("Running with FAST CYTHON BACKEND")
|
||||
elif pigpio:
|
||||
self.backend = RaspiBackend()
|
||||
else:
|
||||
self.logger.warning('Running with dummy backend device')
|
||||
self.logger.warning("Running with dummy backend device")
|
||||
self.backend = SerialBackend()
|
||||
|
||||
def initialize(self):
|
||||
self.logger.info('Initializing... %r backend', self.backend)
|
||||
self.logger.info("Initializing... %r backend", self.backend)
|
||||
self.backend.open()
|
||||
# here should IO / connection initizliation go
|
||||
|
||||
|
@ -94,22 +98,32 @@ class MDBDevice(object):
|
|||
for b in range(0, len(data), 2):
|
||||
if data[b + 1]:
|
||||
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)
|
||||
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.current_request.reset(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)
|
||||
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])
|
||||
self.logger.warning("Received unexpected data: 0x%02x", data[b])
|
||||
|
||||
if self.current_request and not self.current_request.processed:
|
||||
try:
|
||||
|
@ -121,18 +135,20 @@ class MDBDevice(object):
|
|||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.logger.exception('Request process failed!')
|
||||
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)
|
||||
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.logger.debug(
|
||||
">> [%d bytes] %s", len(data), " ".join(["0x%02x" % b for b in data])
|
||||
)
|
||||
|
||||
self.backend.write(msg)
|
||||
|
||||
|
@ -140,26 +156,29 @@ class MDBDevice(object):
|
|||
# Unimplemented...
|
||||
return
|
||||
|
||||
|
||||
#
|
||||
# This is mostly working cashless device implementation
|
||||
#
|
||||
|
||||
|
||||
class CashlessMDBDevice(MDBDevice):
|
||||
base_address = CASHLESS_RESET
|
||||
state = 'IDLE'
|
||||
state = "IDLE"
|
||||
last_poll = 0
|
||||
config_data = [
|
||||
0x01, # Feature level
|
||||
0x19, 0x85, # PLN x---DD
|
||||
0x19,
|
||||
0x85, # PLN x---DD
|
||||
1, # scaling factor
|
||||
0x02, # decimal places factor
|
||||
10, # 10s response time
|
||||
0x00, # misc options...
|
||||
]
|
||||
|
||||
manufacturer = 'GMD'
|
||||
serial_number = '123456789012'
|
||||
model_number = '123456789012'
|
||||
manufacturer = "GMD"
|
||||
serial_number = "123456789012"
|
||||
model_number = "123456789012"
|
||||
software_version = (0x21, 0x37)
|
||||
|
||||
lockup_counter = 0
|
||||
|
@ -193,8 +212,8 @@ class CashlessMDBDevice(MDBDevice):
|
|||
self.lockup_counter = 0
|
||||
|
||||
if req.command == CASHLESS_RESET:
|
||||
self.state = 'RESET'
|
||||
self.logger.info('RESET: Device reset')
|
||||
self.state = "RESET"
|
||||
self.logger.info("RESET: Device reset")
|
||||
self.poll_queue.put([0x00])
|
||||
|
||||
return []
|
||||
|
@ -209,89 +228,95 @@ class CashlessMDBDevice(MDBDevice):
|
|||
pass
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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 (%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):
|
||||
# accept. two latter bytes are value subtracted from balance
|
||||
# displayed after purchase FIXME
|
||||
return [0x05, 0x00, 0xff]
|
||||
return [0x05, 0x00, 0xFF]
|
||||
else:
|
||||
# welp?
|
||||
return [0x06]
|
||||
|
||||
elif req.data[0] == 0x01: # vend cancel
|
||||
self.logger.info('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)
|
||||
self.logger.info("VEND: success %r", req)
|
||||
return []
|
||||
|
||||
elif req.data[0] == 0x03:
|
||||
self.logger.info('VEND: failure')
|
||||
self.logger.info("VEND: failure")
|
||||
return []
|
||||
|
||||
elif req.data[0] == 0x04:
|
||||
self.logger.info('VEND: session complete %r', req)
|
||||
self.state = 'OK'
|
||||
self.logger.info("VEND: session complete %r", req)
|
||||
self.state = "OK"
|
||||
return [0x07]
|
||||
|
||||
elif req.data[0] == 0x05:
|
||||
self.logger.info('VEND: cash sale')
|
||||
self.logger.info("VEND: cash sale")
|
||||
return []
|
||||
|
||||
elif req.data[0] == 0x06:
|
||||
self.logger.info('VEND: negative vend request')
|
||||
self.logger.info("VEND: negative vend request")
|
||||
return [0x06] # deny
|
||||
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:
|
||||
self.logger.info('EXP_ID request: %r', req)
|
||||
self.logger.info("EXP_ID request: %r", req)
|
||||
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)
|
||||
bytearray([0x09])
|
||||
+ bytearray( # peripheral ID
|
||||
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:
|
||||
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.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 = 'OK'
|
||||
self.state = "OK"
|
||||
|
||||
return [0x01] + self.config_data
|
||||
|
||||
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 []
|
||||
|
||||
elif req.command == CASHLESS_READER:
|
||||
self.logger.info('READER update: %r', req.data[0])
|
||||
self.logger.info("READER update: %r", req.data[0])
|
||||
return []
|
||||
|
||||
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
|
||||
if 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):
|
||||
# Cancels current session
|
||||
|
@ -310,19 +335,34 @@ class CashlessMDBDevice(MDBDevice):
|
|||
# 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,
|
||||
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')
|
||||
raise Exception("Invalid amount")
|
||||
|
||||
while amount > 0:
|
||||
bills_list = filter(lambda v: v <= amount, self.bills)
|
||||
|
|
14
mdb/utils.py
14
mdb/utils.py
|
@ -1,14 +1,18 @@
|
|||
from functools import reduce
|
||||
|
||||
|
||||
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 \
|
||||
1000 * ((b & 0xf000) >> 12) + \
|
||||
100 * ((b & 0xf00) >> 8) + \
|
||||
10 * ((b & 0xf0) >> 4) + \
|
||||
(b & 0x0f)
|
||||
return (
|
||||
1000 * ((b & 0xF000) >> 12)
|
||||
+ 100 * ((b & 0xF00) >> 8)
|
||||
+ 10 * ((b & 0xF0) >> 4)
|
||||
+ (b & 0x0F)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue