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 %} 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 @@ NameBalance - {% 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 @@ NameAmountPurchases - {% 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'], +)