diff --git a/.gitignore b/.gitignore
index e1df54d..5b6a15c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
cython-tests/*
.ropeproject
cygpio/*
+result
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0b89ab0
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+graft bitvend/static
+graft bitvend/templates
+global-exclude *.pyc
diff --git a/bitvend.py b/bitvend-run.py
similarity index 94%
rename from bitvend.py
rename to bitvend-run.py
index 589ad98..0cbd89a 100644
--- a/bitvend.py
+++ b/bitvend-run.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python3
+
import logging
logging.basicConfig(level=logging.INFO) # noqa
diff --git a/bitvend/__init__.py b/bitvend/__init__.py
index 0513078..2ab00a6 100644
--- a/bitvend/__init__.py
+++ b/bitvend/__init__.py
@@ -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
diff --git a/bitvend/admin.py b/bitvend/admin.py
index c803181..e36412e 100644
--- a/bitvend/admin.py
+++ b/bitvend/admin.py
@@ -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('/')
diff --git a/bitvend/default_settings.py b/bitvend/default_settings.py
index 8f6e5a6..290679f 100644
--- a/bitvend/default_settings.py
+++ b/bitvend/default_settings.py
@@ -42,4 +42,4 @@ ITEMS = [
},
]
-DEBT_LIMIT = 2500
+DEBT_LIMIT = 1500
diff --git a/bitvend/forms.py b/bitvend/forms.py
index 203b3d5..84805e1 100644
--- a/bitvend/forms.py
+++ b/bitvend/forms.py
@@ -45,5 +45,4 @@ class TransferForm(FlaskForm):
class ManualForm(FlaskForm):
amount = DecimalUnityField("Amount", default=0, validators=[
- NumberRange(min=1),
])
diff --git a/bitvend/processor.py b/bitvend/processor.py
index 91d78e5..2dc4ce0 100644
--- a/bitvend/processor.py
+++ b/bitvend/processor.py
@@ -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:
diff --git a/bitvend/templates/base.html b/bitvend/templates/base.html
index 3a8b49b..4194b94 100644
--- a/bitvend/templates/base.html
+++ b/bitvend/templates/base.html
@@ -77,10 +77,10 @@
{% endif %}
- 1zł = {{ format_btc(from_local_currency(100)) }}
+ 1zł = {{ format_btc(from_local_currency(100, True)) }}
- Rate: {{ to_local_currency(100000000) / 100 }}zł
+ Rate: {{ to_local_currency(100000000, True) / 100 }}zł
diff --git a/bitvend/templates/index.html b/bitvend/templates/index.html
index 1960b67..5f63e7e 100644
--- a/bitvend/templates/index.html
+++ b/bitvend/templates/index.html
@@ -53,8 +53,8 @@
Name | Balance |
- {% for user in hallofshame %}
- {{ user }} | {{ format_currency(user.balance) }} |
+ {% for user, balance in hallofshame %}
+ {{ user }} | {{ format_currency(balance) }} |
{% else %}
Wow! Nobody's due! |
{% endfor %}
@@ -65,8 +65,8 @@
Name | Amount | Purchases |
- {% for user in hallofaddicts %}
- {{ user }} | {{ format_currency(user.purchase_amount) }} | {{ user.purchase_count }} |
+ {% for user, purchase_amount, purchase_count in hallofaddicts %}
+ {{ user }} | {{ format_currency(purchase_amount) }} | {{ purchase_count }} |
{% else %}
Huh? |
{% endfor %}
@@ -132,7 +132,7 @@
{{ format_currency(item.value) }}
- {{ format_btc(from_local_currency(item.value*1.03)) }}
+ {{ format_btc(from_local_currency(item.value*1.03, True)) }}
{{ item.name }}
@@ -140,7 +140,7 @@
- {% 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))) %}
{{ config['INPUT_ADDRESS'] }}
diff --git a/bitvend/utils.py b/bitvend/utils.py
index d360477..24914cf 100644
--- a/bitvend/utils.py
+++ b/bitvend/utils.py
@@ -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):
diff --git a/bitvend/views.py b/bitvend/views.py
index 7e424cb..23e8147 100644
--- a/bitvend/views.py
+++ b/bitvend/views.py
@@ -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) \
diff --git a/cygpio/cygpio.pyx b/cygpio/cygpio.pyx
new file mode 100644
index 0000000..4c689e3
--- /dev/null
+++ b/cygpio/cygpio.pyx
@@ -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)
diff --git a/cygpio/cygpio_test.py b/cygpio/cygpio_test.py
new file mode 100644
index 0000000..a9d8108
--- /dev/null
+++ b/cygpio/cygpio_test.py
@@ -0,0 +1,2 @@
+import cygpio
+cygpio.test()
diff --git a/cygpio/setup.py b/cygpio/setup.py
new file mode 100644
index 0000000..4ce5599
--- /dev/null
+++ b/cygpio/setup.py
@@ -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"])])
+ )
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..de8951b
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,133 @@
+with import {};
+
+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
+ ];
+}
diff --git a/deployment/bitvend.service b/deployment/bitvend.service
index 1bac993..826515b 100644
--- a/deployment/bitvend.service
+++ b/deployment/bitvend.service
@@ -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]
diff --git a/deployment/playbook.yml b/deployment/playbook.yml
index 70a9cfe..c0b2212 100644
--- a/deployment/playbook.yml
+++ b/deployment/playbook.yml
@@ -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
diff --git a/mdb/device.py b/mdb/device.py
index 6c27270..f05144a 100644
--- a/mdb/device.py
+++ b/mdb/device.py
@@ -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 '' % (
@@ -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:
diff --git a/module.nix b/module.nix
new file mode 100644
index 0000000..bdfd06e
--- /dev/null
+++ b/module.nix
@@ -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;
+ '';
+ };
+ };
+ };
+ };
+}
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..eb8c91b
--- /dev/null
+++ b/setup.py
@@ -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'],
+)