*: reformat using black

master
informatic 2023-07-14 23:33:45 +02:00
parent c888873b7a
commit d5896d5d16
20 changed files with 543 additions and 423 deletions

168
accept.py
View File

@ -5,127 +5,129 @@ import threading
import pprint
import requests
INPUT_ADDR = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7' # bitvend addr
#INPUT_ADDR = '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22' # test 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
# 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',
'n': 0,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': False,
'tx_index': 93845150,
'type': 0,
'value': 300000},
'script': '483045022100980b532c7b417b6a7ce4ef1b5e509dda61762ccbbfd35189c2ed7985e6a35d1c02205afacbda7b0e10c6b41100ecee5fc05ab47ca00c85b842923324d55c0db529f8014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
'sequence': 4294967295},
{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
'n': 0,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': False,
'tx_index': 182282259,
'type': 0,
'value': 2978800},
'script': '483045022100a560606252d83635c784139ae271db498e6528d561105d72c892dc80369036d3022048bfe431f3055d115f8faf0bf2b582912fa226a2f871b44f8bef348a278c8cea014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
'sequence': 4294967295},
{'prev_out': {'addr': '17qC9bmMCyaReEPRWksYt99tr8s8YWLrDK',
'n': 0,
'script': '76a9144aee06e79fe2f92c3916b0cc0a78478e2434680e88ac',
'spent': False,
'tx_index': 93651246,
'type': 0,
'value': 407500},
'script': '47304402207b0946f973566ec65f4fb3878285274410e10b758806d8b46407d5219ffb36a1022017fdde92072e94bde68271754000636aa94952b91c2d818ed6d88a08cbf900fb0121029f8261faed04d668a78eadf7a3ef845de409e500dace54e604c547702a67b4fc',
'sequence': 4294967295}],
'lock_time': 0,
'out': [{'addr': '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22',
'n': 0,
'script': '76a914e17465c3ef40d44508c7e923140d54c3cd8ced3d88ac',
'spent': True,
'tx_index': 209616370,
'type': 0,
'value': 1251574},
{'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
'n': 1,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': True,
'tx_index': 209616370,
'type': 0,
'value': 2424726}],
'relayed_by': '5.189.53.123',
'size': 585,
'time': 1484319619,
'tx_index': 209616370,
'ver': 1,
'vin_sz': 3,
'vout_sz': 2}})
exit(0)
'''
"""
process_transaction({'op': 'utx',
'x': {'hash': '03ed0015c29dfc3bfd3d8a215490d82e3562fe4e8bf5f2ffa737ac8fdc850cc4',
'inputs': [{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
'n': 0,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': False,
'tx_index': 93845150,
'type': 0,
'value': 300000},
'script': '483045022100980b532c7b417b6a7ce4ef1b5e509dda61762ccbbfd35189c2ed7985e6a35d1c02205afacbda7b0e10c6b41100ecee5fc05ab47ca00c85b842923324d55c0db529f8014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
'sequence': 4294967295},
{'prev_out': {'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
'n': 0,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': False,
'tx_index': 182282259,
'type': 0,
'value': 2978800},
'script': '483045022100a560606252d83635c784139ae271db498e6528d561105d72c892dc80369036d3022048bfe431f3055d115f8faf0bf2b582912fa226a2f871b44f8bef348a278c8cea014104e2a76bdeaa387cae1cf920a9a8b54ee4c5e7378ea1985a638f2f3ec609a1d6a54e49e85d10bec55ce9321c5a45e2b21ca8eb1a3e18635405c4812d8467339e9c',
'sequence': 4294967295},
{'prev_out': {'addr': '17qC9bmMCyaReEPRWksYt99tr8s8YWLrDK',
'n': 0,
'script': '76a9144aee06e79fe2f92c3916b0cc0a78478e2434680e88ac',
'spent': False,
'tx_index': 93651246,
'type': 0,
'value': 407500},
'script': '47304402207b0946f973566ec65f4fb3878285274410e10b758806d8b46407d5219ffb36a1022017fdde92072e94bde68271754000636aa94952b91c2d818ed6d88a08cbf900fb0121029f8261faed04d668a78eadf7a3ef845de409e500dace54e604c547702a67b4fc',
'sequence': 4294967295}],
'lock_time': 0,
'out': [{'addr': '1MZ6UbznUjoc34pkYyyofWJY42fAoA6k22',
'n': 0,
'script': '76a914e17465c3ef40d44508c7e923140d54c3cd8ced3d88ac',
'spent': True,
'tx_index': 209616370,
'type': 0,
'value': 1251574},
{'addr': '1infuHrvt7tuGY8nJcfTwB3wwAR1XwUcW',
'n': 1,
'script': '76a91407e72dad6fc3a60ae5c4973bf2a23750713870b188ac',
'spent': True,
'tx_index': 209616370,
'type': 0,
'value': 2424726}],
'relayed_by': '5.189.53.123',
'size': 585,
'time': 1484319619,
'tx_index': 209616370,
'ver': 1,
'vin_sz': 3,
'vout_sz': 2}})
exit(0)
"""
websocket.enableTrace(True)
ws = websocket.WebSocketApp("wss://ws.blockchain.info/inv",
on_message = on_message,
on_error = on_error,
on_close = on_close)
ws = websocket.WebSocketApp(
"wss://ws.blockchain.info/inv",
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.on_open = on_open
print("### running... ###")
ws.run_forever()

View File

@ -2,13 +2,14 @@
import logging
logging.basicConfig(level=logging.DEBUG) # noqa
logging.basicConfig(level=logging.DEBUG) # noqa
import threading
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()
#proc.start()
threading.Thread(
target=app.run,
kwargs={
"host": "0.0.0.0",
},
daemon=True,
).start()
# proc.start()
dev.run()

View File

@ -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

View File

@ -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("/")

View File

@ -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'
# 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,
},
]

View File

@ -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=[
NumberRange(min=1),
])
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=[])

View File

@ -6,12 +6,14 @@ from bitvend.models import db, Transaction
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()
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())
@ -22,18 +24,16 @@ def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
return [
type(default)(date_hash[d]) if d in date_hash else default
for d in daterange(date_from, date_to)
]
]
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(
#func.date_trunc('day', Transaction.created),
func.strftime('%Y-%m-%d', Transaction.created),
func.count(Transaction.created),
).filter(Transaction.type == 'purchase'), date_from, date_to),
])
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.count(Transaction.created),
).filter(Transaction.type == "purchase"),
date_from,
date_to,
),
]
)

View File

@ -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()

View File

@ -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,22 +103,24 @@ 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)
#value = db.Column(db.Integer)
# value = db.Column(db.Integer)
@hybrid_property
def value(self):
return self.amount
@ -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)

View File

@ -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,97 +30,103 @@ 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" \
% (self.chain_id, self.token),
"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,
)
)
db.session.add(intx)
db.session.commit()
outtx = Transaction(
uid='__bitcoin__',
type='purchase',
)
uid="__bitcoin__",
type="purchase",
)
db.session.add(outtx)
db.session.commit()
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)
def on_message(self, ws, message):
#print 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({
"event": "tx-confidence",
"address": self.input_address,
"confidence": 0.9,
}))
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

View File

@ -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"
)

View File

@ -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(".")

View File

@ -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,92 +12,103 @@ 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,
hallofaddicts=hallofaddicts,
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())

View File

@ -1,2 +1,3 @@
import cygpio
cygpio.test()

View File

@ -3,5 +3,5 @@ from distutils.extension import Extension
from Cython.Build import cythonize
setup(
ext_modules = cythonize([Extension("cygpio", ["cygpio.pyx"], libraries=["pigpio"])])
)
ext_modules=cythonize([Extension("cygpio", ["cygpio.pyx"], libraries=["pigpio"])])
)

View File

@ -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:
@ -70,9 +70,9 @@ class RaspiBackend(Backend):
wid = self.pi.wave_create()
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)
self.pi.wave_delete(wid)
@ -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

View File

@ -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

View File

@ -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
@ -38,7 +39,7 @@ class MDBRequest(object):
try:
if self.ack:
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])
else:
@ -46,18 +47,21 @@ 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(),
)
class MDBDevice(object):
@ -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
@ -92,24 +96,34 @@ class MDBDevice(object):
while True:
data = self.backend.read()
for b in range(0, len(data), 2):
if data[b+1]:
if self.current_request: # and not self.current_request.processed:
if self.current_request.command not in [0xf2]:
if data[b + 1]:
if self.current_request: # and not self.current_request.processed:
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)
#self.send(self.send_buffer, checksum=False)
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,46 +156,49 @@ 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
1, # scaling factor
0x02, # decimal places factor
10, # 10s response time
0x00, # misc options...
]
0x01, # Feature level
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
def process_request(self, req):
# 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
# if self.lockup_counter % 50 == 0:
# self.logger.info('YOLO')
# return []
# return
#if req.command == 0x31 and req.validate_checksum():
# if req.command == 0x31 and req.validate_checksum():
# return []
#if req.command == 0x37 and req.validate_checksum():
# if req.command == 0x37 and req.validate_checksum():
# return []
#if req.command == 0x36 and req.validate_checksum():
# if req.command == 0x36 and req.validate_checksum():
# return []
#if req.command == 0x34 and req.validate_checksum():
# if req.command == 0x34 and req.validate_checksum():
# return []
if (req.command & self.base_address) != self.base_address:
@ -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)
if req.data[0] == 0x00: # vend request
self.logger.info("VEND: request %r", req)
value, product_bcd = struct.unpack(">xhhx", req.data)
product = bcd_decode(product_bcd)
self.logger.info('VEND: requested %d (%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')
return [0x06] # deny == ok
elif req.data[0] == 0x01: # vend cancel
self.logger.info("VEND: cancel")
return [0x06] # deny == ok
elif req.data[0] == 0x02: # vend succ
self.logger.info('VEND: success %r', req)
elif req.data[0] == 0x02: # vend succ
self.logger.info("VEND: success %r", req)
return []
elif req.data[0] == 0x03:
self.logger.info('VEND: failure')
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')
return [0x06] # deny
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)

View File

@ -1,14 +1,18 @@
from functools import reduce
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):
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)
)

View File

@ -5,5 +5,5 @@ setup(
version="1.0",
packages=find_packages(),
include_package_data=True,
scripts=['bitvend-run.py'],
scripts=["bitvend-run.py"],
)