Merge branch 'master' of https://code.hackerspace.pl/q3k/bitvend into usb-interface

master
informatic 2020-06-22 14:11:07 +02:00
commit f4f76f888e
21 changed files with 423 additions and 22 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
cython-tests/*
.ropeproject
cygpio/*
result

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
graft bitvend/static
graft bitvend/templates
global-exclude *.pyc

View File

@ -1,3 +1,5 @@
#!/usr/bin/env python3
import logging
logging.basicConfig(level=logging.INFO) # noqa

View File

@ -30,6 +30,7 @@ def bitvend_user_loader(username, profile=None):
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)
# Use proper proxy headers, this fixes invalid scheme in

View File

@ -1,6 +1,7 @@
from flask import Blueprint, render_template, redirect, request, flash, url_for
from flask_login import current_user, fresh_login_required
from bitvend import dev
from bitvend.models import db, Transaction
from bitvend.forms import ManualForm
from spaceauth import cap_required
@ -31,3 +32,21 @@ def transactions(page):
return render_template('admin/transactions.html',
transactions=Transaction.query.paginate(page)
)
@bp.route('/begin')
@fresh_login_required
@admin_required
def begin():
dev.begin_session(500)
flash('Operation successful.', 'success')
return redirect('/')
@bp.route('/cancel')
@fresh_login_required
@admin_required
def cancel():
dev.cancel_session()
flash('Operation successful.', 'success')
return redirect('/')

View File

@ -42,4 +42,4 @@ ITEMS = [
},
]
DEBT_LIMIT = 2500
DEBT_LIMIT = 1500

View File

@ -45,5 +45,4 @@ class TransferForm(FlaskForm):
class ManualForm(FlaskForm):
amount = DecimalUnityField("Amount", default=0, validators=[
NumberRange(min=1),
])

View File

@ -24,7 +24,7 @@ class PaymentProcessor(threading.Thread):
self.chain_id = chain_id
self.logger = logging.getLogger(type(self).__name__)
self.token = token
if app:
self.init_app(app)
@ -42,12 +42,12 @@ class PaymentProcessor(threading.Thread):
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:

View File

@ -77,10 +77,10 @@
{% endif %}
<p class="navbar-text navbar-right">
1zł = {{ format_btc(from_local_currency(100)) }}
1zł = {{ format_btc(from_local_currency(100, True)) }}
</p>
<p class="navbar-text navbar-right">
<b>Rate:</b> {{ to_local_currency(100000000) / 100 }}zł
<b>Rate:</b> {{ to_local_currency(100000000, True) / 100 }}zł
</p>
</div>

View File

@ -53,8 +53,8 @@
<thead><tr>
<th>Name</th><th class="text-right">Balance</th>
</tr></thead>
{% for user in hallofshame %}
<tr><td>{{ user }}</td><td class="text-right">{{ format_currency(user.balance) }}</td></tr>
{% for user, balance in hallofshame %}
<tr><td>{{ user }}</td><td class="text-right">{{ format_currency(balance) }}</td></tr>
{% else %}
<tr><td colspan=2 class="placeholder">Wow! Nobody's due!</td></tr>
{% endfor %}
@ -65,8 +65,8 @@
<thead><tr>
<th>Name</th><th class="text-right">Amount</th><th>Purchases</th>
</tr></thead>
{% for user in hallofaddicts %}
<tr><td>{{ user }}</td><td class="text-right">{{ format_currency(user.purchase_amount) }}</td><td>{{ user.purchase_count }}</td></tr>
{% for user, purchase_amount, purchase_count in hallofaddicts %}
<tr><td>{{ user }}</td><td class="text-right">{{ format_currency(purchase_amount) }}</td><td>{{ purchase_count }}</td></tr>
{% else %}
<tr><td colspan=3 class="placeholder">Huh?</td></tr>
{% endfor %}
@ -132,7 +132,7 @@
<div class="col-md-12">
<div class="pull-right">
<span class="label label-info">{{ format_currency(item.value) }}</span>
<span class="label label-primary">{{ format_btc(from_local_currency(item.value*1.03)) }}</span>
<span class="label label-primary">{{ format_btc(from_local_currency(item.value*1.03, True)) }}</span>
</div>
<h3>{{ item.name }}</h3>
</div>
@ -140,7 +140,7 @@
<img src="{{ item.image }}" class="img-responsive center-block" />
</div>
<div class="col-xs-6 text-center">
{% with btc_uri = 'bitcoin:%s?amount=%s' % (config['INPUT_ADDRESS'], sat_to_btc(from_local_currency(item.value*1.03))) %}
{% with btc_uri = 'bitcoin:%s?amount=%s' % (config['INPUT_ADDRESS'], sat_to_btc(from_local_currency(item.value*1.03, True))) %}
<a href="{{ btc_uri }}">
<img src="{{ qrcode(btc_uri) }}" class="img-responsive center-block"/>
<code><small>{{ config['INPUT_ADDRESS'] }}</small></code>

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import cachetools
import requests
@ -6,14 +8,25 @@ def get_exchange_rate(currency='PLN'):
# Returns current exchange rate for selected currency
return requests.get('https://blockchain.info/pl/ticker').json()[currency]['last']
def to_local_currency(sat):
def to_local_currency(sat, safe=False):
# Returns satoshi in local lowest denomination currency (grosze)
rate = get_exchange_rate()
try:
rate = get_exchange_rate()
except:
if safe:
return 0
raise
return int(sat / 1000000.0 * rate)
def from_local_currency(val):
def from_local_currency(val, safe=False):
# Returns satoshi value from local currency
rate = get_exchange_rate()
try:
rate = get_exchange_rate()
except:
if safe:
return 0
raise
return int(val / rate * 1000000)
def sat_to_btc(amount):

View File

@ -19,12 +19,14 @@ bp = Blueprint('bitvend', __name__, template_folder='templates')
def index():
transactions = []
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) \

98
cygpio/cygpio.pyx Normal file
View File

@ -0,0 +1,98 @@
RX_PIN = 4
TX_PIN = 17
cdef extern from "pigpio.h":
int gpioInitialise()
int gpioCfgInterfaces(unsigned ifFlags)
int gpioSetMode(unsigned gpio, unsigned mode)
int gpioSerialReadOpen(unsigned user_gpio, unsigned baud, unsigned data_bits)
int gpioSerialRead(unsigned user_gpio, void *buf, size_t bufSize) nogil
int gpioSerialReadClose(unsigned user_gpio)
int gpioWaveCreate()
int gpioWaveDelete(unsigned wave_id)
int gpioWaveClear()
int gpioWaveTxSend(unsigned wave_id, unsigned wave_mode)
int gpioWaveTxBusy()
int gpioWaveAddSerial(unsigned user_gpio, unsigned baud, unsigned data_bits, unsigned stop_bits, unsigned offset, unsigned numBytes, char *str)
int gpioCfgMemAlloc(unsigned memAllocMode)
unsigned int gpioCfgGetInternals()
int gpioCfgSetInternals(unsigned int cfgVal)
cdef int INPUT "PI_INPUT"
cdef int OUTPUT "PI_OUTPUT"
cdef int PI_DISABLE_FIFO_IF
cdef int PI_DISABLE_SOCK_IF
cdef int PI_WAVE_MODE_ONE_SHOT
cdef unsigned PI_MEM_ALLOC_PAGEMAP
cdef extern from "unistd.h" nogil:
unsigned int sleep(unsigned int seconds)
unsigned int usleep(unsigned int usecs)
def test():
b = CythonRaspiBackend()
b.open()
while True:
print(repr(b.read()))
cdef class CythonRaspiBackend(object):
cdef int rx_pin
cdef int tx_pin
def __init__(self, rx_pin=RX_PIN, tx_pin=TX_PIN):
self.rx_pin = rx_pin
self.tx_pin = tx_pin
cpdef open(self):
# Enable startup debug
gpioCfgSetInternals(gpioCfgGetInternals() | 8);
# Force usage of non-mailbox DMA
gpioCfgMemAlloc(PI_MEM_ALLOC_PAGEMAP);
gpioCfgInterfaces(PI_DISABLE_FIFO_IF | PI_DISABLE_SOCK_IF);
gpioInitialise()
gpioWaveClear()
gpioSetMode(self.tx_pin, INPUT)
# gpioSerClose...
cdef int resp = gpioSerialReadOpen(self.rx_pin, 9600, 9)
if resp != 0:
raise Exception('Serial open failed: %d' % resp)
cpdef read(self):
cdef unsigned char buf[1024]
cdef int read_size
with nogil:
while 1:
read_size = gpioSerialRead(self.rx_pin, &buf, sizeof(buf))
if read_size > 0:
break
usleep(100)
return bytes(buf[0:read_size])
cpdef write(self, data):
cdef char* c_data = data
gpioWaveAddSerial(self.tx_pin, 9600, 9, 6, 0, len(data), c_data)
wid = gpioWaveCreate()
gpioSetMode(self.tx_pin, OUTPUT)
gpioWaveTxSend(wid, PI_WAVE_MODE_ONE_SHOT)
while gpioWaveTxBusy():
usleep(100)
gpioWaveDelete(wid)
gpioSetMode(self.tx_pin, INPUT)

2
cygpio/cygpio_test.py Normal file
View File

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

7
cygpio/setup.py Normal file
View File

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

133
default.nix Normal file
View File

@ -0,0 +1,133 @@
with import <nixpkgs> {};
let
upstream = with pkgs.python3Packages; {
inherit buildPythonPackage;
inherit fetchPypi;
inherit blinker;
inherit cachetools;
inherit cryptography;
inherit cython;
inherit flask;
inherit flask_login;
inherit flask_sqlalchemy;
inherit flask_wtf;
inherit mock;
inherit prometheus_client;
inherit pyjwt;
inherit pytest;
inherit qrcode;
inherit raspberrypi-tools;
inherit requests;
inherit six;
};
in with upstream; let
websocket_client = buildPythonPackage rec {
version = "0.40.0";
pname = "websocket_client";
src = fetchPypi {
inherit pname version;
sha256 = "1yz67wdjijrvwpx0a0f6wdfy8ajsvr9xbj5514ld452fqnh19b20";
};
propagatedBuildInputs = [
six
];
};
oauthlib = buildPythonPackage rec {
pname = "oauthlib";
version = "2.1.0";
src = fetchPypi {
inherit pname version;
sha256 = "0qj183fipjzw6ipiv2k10896y97sxvargnkb6db5qs61c5d6cddc";
};
checkInputs = [ mock pytest ];
propagatedBuildInputs = [ cryptography blinker pyjwt ];
checkPhase = ''
py.test tests/
'';
};
requests_oauthlib = buildPythonPackage rec {
pname = "requests-oauthlib";
version = "1.0.0";
src = fetchPypi {
inherit pname version;
sha256 = "0gys581rqjdlv0whhqp5s2caxx66jzvb2hslxn8v7bypbbnbz1l8";
};
doCheck = false;
propagatedBuildInputs = [ oauthlib requests ];
};
flask_oauthlib = buildPythonPackage rec {
pname = "Flask-OAuthlib";
version = "0.9.5";
src = fetchPypi {
inherit pname version;
sha256 = "01llysn53jfrr9n02hvjcynrb28lh4rjqn18k2hhk6an09cq7znb";
};
doCheck = false;
propagatedBuildInputs = [ flask flask_sqlalchemy requests_oauthlib oauthlib ];
};
spaceauth = buildPythonPackage rec {
pname = "Flask-SpaceAuth";
version = "0.2.0";
src = pkgs.fetchgit {
url = "https://code.hackerspace.pl/informatic/flask-spaceauth";
rev = "v${version}";
sha256 = "000vg41lw4pyd10bvcqrp15y673qlpkllgppfhm48w7vk02r6zi2";
};
propagatedBuildInputs = [ flask flask_login flask_oauthlib flask_wtf requests ];
};
pigpio = stdenv.mkDerivation rec {
pname = "pigpio";
version = "74-q3k";
buildFlags = [ "STRIPLIB=echo" "STRIP=echo" "CFLAGS=-g" ];
installFlags = [ "DESTDIR=$(out)" "prefix=" ];
src = pkgs.fetchFromGitHub {
owner = "q3k";
repo = "pigpio";
rev = "fa8c3ec41cb70da4d1868caec655d5f7d474573f";
sha256 = "0shd2p1w8k0iz7v5j81w8hw6hy67zxd6r4mvz2xflabiwblr5zi3";
};
dontStrip = true;
propagatedBuildInputs = [ raspberrypi-tools ];
};
cygpio = buildPythonPackage {
pname = "cygpio";
version = "1.0.0";
src = ./cygpio;
propagatedBuildInputs = [ pigpio cython ];
};
in buildPythonPackage rec {
name = "bitvend";
src = ./.;
doCheck = false;
propagatedBuildInputs = [
cygpio
flask
flask_sqlalchemy
websocket_client
cachetools
requests
prometheus_client
spaceauth
qrcode
];
}

View File

@ -7,7 +7,7 @@ Type=simple
User=bitvend
Environment=BITVEND_SETTINGS=bitvend.cfg
WorkingDirectory=/var/bitvend
ExecStart=/usr/bin/python3 -u /var/bitvend/bitvend.py
ExecStart=/usr/bin/python3 -u /var/bitvend/bitvend-run.py
Restart=on-failure
[Install]

View File

@ -1,5 +1,11 @@
- hosts: bitvend
tasks:
- hostname: name={{ inventory_hostname }}
- apt: name=dphys-swapfile state=absent
- file: name=/var/swap state=absent
- mount: name=/var/log src=tmpfs fstype=tmpfs state=present opts="defaults,noatime,nosuid,mode=0755,size=50m"
- apt: name="{{ item }}" state=present
with_items:
- pigpio

View File

@ -7,6 +7,12 @@ try:
except ImportError:
import Queue as queue
try:
import cygpio
except ImportError:
raise
cygpio = None
from mdb.utils import compute_checksum, compute_chk, bcd_decode
from mdb.constants import *
from mdb.backend import RaspiBackend, DummyBackend, SerialBackend, pigpio
@ -18,6 +24,10 @@ class MDBRequest(object):
processed = False
def __init__(self, command):
self.reset(command)
def reset(self, command):
self.processed = False
self.timestamp = time.time()
self.command = command
self.data = bytearray()
@ -41,7 +51,7 @@ class MDBRequest(object):
return False
@property
def ack(self):
return self.data[-1] == 0x00
return len(self.data) and self.data[-1] == 0x00
def __repr__(self):
return '<MDBRequest 0x%02x [%s] chk:%r>' % (
@ -60,20 +70,26 @@ class MDBDevice(object):
def __init__(self, app=None):
self.logger = logging.getLogger(type(self).__name__)
self.poll_queue = queue.Queue()
if pigpio:
if cygpio:
self.backend = cygpio.CythonRaspiBackend()
self.logger.warning('Running with FAST CYTHON BACKEND')
elif pigpio:
self.backend = RaspiBackend()
else:
self.logger.warning('Running with dummy backend device')
self.backend = SerialBackend()
def initialize(self):
self.logger.info('Initializing...')
self.logger.info('Initializing... %r backend', self.backend)
self.backend.open()
# here should IO / connection initizliation go
def run(self):
self.initialize()
self.current_request = MDBRequest(0)
self.current_request.processed = True
while True:
data = self.backend.read()
for b in range(0, len(data), 2):
@ -85,7 +101,7 @@ class MDBDevice(object):
self.logger.info('Got response: %d',self.current_request.data[-1])
self.poll_msg = []
self.current_request = MDBRequest(data[b])
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:

90
module.nix Normal file
View File

@ -0,0 +1,90 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) mkIf mkOption types;
cfg = config.services.bitvend;
bitvend = (import ./default.nix);
cfgFile = pkgs.writeText "bitvend.cfg"
''
SQLALCHEMY_DATABASE_URI = 'sqlite:///${cfg.stateDir}/bitvend.db'
SPACEAUTH_CONSUMER_KEY = '${cfg.spaceauthConsumerKey}'
SPACEAUTH_CONSUMER_SECRET = '${cfg.spaceauthConsumerSecret}'
BLOCKCYPHER_TOKEN = '${cfg.blockcypherToken}'
SECRET_KEY = '${cfg.secretKey}'
'';
in {
options.services.bitvend = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable bitvend";
};
stateDir = mkOption {
type = types.path;
default = "/var/db/bitvend";
description = "Location of bitvend's config/data directory";
};
spaceauthConsumerKey = mkOption {
type = types.str;
default = "";
description = "spaceauth consumer key";
};
spaceauthConsumerSecret = mkOption {
type = types.str;
default = "";
description = "spaceauth consumer secret";
};
blockcypherToken = mkOption {
type = types.str;
default = "";
description = "blockcypher token";
};
secretKey = mkOption {
type = types.str;
default = "";
description = "blockcypher token";
};
hostName = mkOption {
type = types.str;
default = "vending.waw.hackerspace.pl";
description = "hostname";
};
};
config = mkIf cfg.enable {
systemd.services.bitvend = {
environment = {
BITVEND_SETTINGS = cfgFile;
};
wantedBy = [ "multi-user.target" ];
script = ''
${bitvend}/bin/bitvend-run.py
'';
};
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' 0750 'root' 'root' - -"
];
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.nginx = {
enable = true;
appendHttpConfig = ''
proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=qrcode_cache:10m max_size=50m inactive=60m;
'';
virtualHosts."${cfg.hostName}" = {
locations."/" = {
proxyPass = "http://127.0.0.1:5000";
};
locations."/qrcode/" = {
proxyPass = "http://127.0.0.1:5000";
extraConfig = ''
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache qrcode_cache;
'';
};
};
};
};
}

9
setup.py Normal file
View File

@ -0,0 +1,9 @@
from setuptools import setup, find_packages
setup(
name="bitvend",
version="1.0",
packages=find_packages(),
include_package_data=True,
scripts=['bitvend-run.py'],
)